Skip to main content
Glama
algorand-api.js30.4 kB
const algosdk = require('algosdk'); const { getAlgokit } = require('@algorandfoundation/algokit-utils'); const axios = require('axios'); class AlgorandAPI { constructor(config = {}) { this.network = config.network || 'mainnet'; // Explorer URLs for different networks this.explorerUrls = { mainnet: { lora: 'https://lora.algokit.io/mainnet', pera: 'https://explorer.perawallet.app' }, testnet: { lora: 'https://lora.algokit.io/testnet', pera: 'https://testnet.explorer.perawallet.app' }, betanet: { lora: 'https://lora.algokit.io/betanet' } }; // Get current network explorer URLs this.explorer = this.explorerUrls[this.network] || this.explorerUrls.mainnet; // Popular Algorand assets mapping (symbol -> assetId) // Network-specific asset IDs this.assetMapping = this.network === 'testnet' ? { // TestNet asset IDs (these are common test assets) 'USDC': 10458941, // USDC on TestNet 'ALGO': 0, // Native ALGO // Note: You can add more testnet assets here as you discover them // TestNet assets often have different IDs than mainnet } : { // MainNet asset IDs // Stablecoins 'USDC': 31566704, // Circle USD Coin 'USDT': 312769, // Tether USD (Algorand) 'STBL': 465865291, // AlgoFi Stablecoin 'GOBTC': 386192725, // AlgoMint Bitcoin 'GOETH': 386195940, // AlgoMint Ethereum // DeFi Tokens 'ALGO': 0, // Native Algorand (special case) 'BANK': 900652777, // AlgoFi Governance Token 'STKE': 511484048, // AlgoStake Token 'OPUL': 287867876, // Opulous Token 'YLDY': 226701642, // Yieldly Token 'TINY': 378382099, // TinymanSwap Token 'DEFLY': 470842789, // Defly Token 'VOTE': 452399768, // AlgoFi Voting Token 'GARD': 684649988, // GARD Protocol Token // Gaming & NFT 'CHIPS': 388592191, // AlgoChips 'SMILE': 300208676, // Smile Coin 'ZONE': 444035862, // Zone Gaming Token // Meme Coins 'AKITA': 523683256, // Akita Inu 'KITSU': 511593751, // Kitsu Inu // Wrapped Assets 'WBTC': 1058926737, // Wrapped Bitcoin (Portal) 'WETH': 1058926738, // Wrapped Ethereum (Portal) 'WSOL': 1033767555, // Wrapped Solana (Portal) 'WAVAX': 1058926739, // Wrapped Avalanche (Portal) // Other Popular Assets 'PLANETS': 27165954, // PlanetWatch 'XET': 283820866, // Xfinite Token 'ARCC': 163650, // Asia Reserve Currency 'CURATOR': 319305714, // Curator Token 'ALCH': 310014962, // AlcheCoin 'BIRDS': 478549868, // BirdBot Token 'NEKOS': 404044168, // Nekoin 'ALGX': 724480511, // Algoxygen 'ASASTATS': 393537671, // ASAStats Token 'BOARD': 342889824, // Board Token 'HDL': 137594422, // Headline Token 'CHOICE': 297995609, // Choice Coin 'GEMS': 230946361, // AlgoGems 'ACORN': 226265212 // Acorn Token }; // Initialize Algorand client with custom or default endpoints let baseServer, indexerServer; // Check for custom endpoints first if (process.env.ALGORAND_NODE_URL) { baseServer = process.env.ALGORAND_NODE_URL; } else { // Use default endpoints based on network if (this.network === 'mainnet') { baseServer = 'https://mainnet-api.algonode.cloud'; } else if (this.network === 'testnet') { baseServer = 'https://testnet-api.algonode.cloud'; } else if (this.network === 'betanet') { baseServer = 'https://betanet-api.algonode.cloud'; } else { throw new Error(`Unknown network: ${this.network}`); } } if (process.env.ALGORAND_INDEXER_URL) { indexerServer = process.env.ALGORAND_INDEXER_URL; } else { // Use default indexer endpoints based on network if (this.network === 'mainnet') { indexerServer = 'https://mainnet-idx.algonode.cloud'; } else if (this.network === 'testnet') { indexerServer = 'https://testnet-idx.algonode.cloud'; } else if (this.network === 'betanet') { indexerServer = 'https://betanet-idx.algonode.cloud'; } } this.algodClient = new algosdk.Algodv2('', baseServer, ''); this.indexerClient = new algosdk.Indexer('', indexerServer, ''); // Load configured account from environment this.configuredAccount = null; this.configuredAddress = process.env.ALGORAND_ACCOUNT_ADDRESS || null; if (process.env.ALGORAND_ACCOUNT_MNEMONIC) { try { this.configuredAccount = algosdk.mnemonicToSecretKey(process.env.ALGORAND_ACCOUNT_MNEMONIC); // Override address if mnemonic is provided (mnemonic takes precedence) this.configuredAddress = this.configuredAccount.addr; console.log(`Configured account loaded: ${this.configuredAddress}`); } catch (error) { console.error('Failed to load account from mnemonic:', error.message); } } else if (process.env.ALGORAND_ACCOUNT_PRIVATE_KEY) { try { const privateKey = Buffer.from(process.env.ALGORAND_ACCOUNT_PRIVATE_KEY, 'base64'); // For operations that need signing this.configuredAccount = { sk: privateKey, addr: this.configuredAddress }; console.log(`Configured account loaded from private key: ${this.configuredAddress}`); } catch (error) { console.error('Failed to load account from private key:', error.message); } } } // Get test account if configured getTestAccount() { if (!this.configuredAddress) { throw new Error('No account configured. Set ALGORAND_ACCOUNT_ADDRESS or ALGORAND_ACCOUNT_MNEMONIC in .env file'); } const result = { address: this.configuredAddress, network: this.network }; // Only include private key if we have it if (this.configuredAccount && this.configuredAccount.sk) { result.privateKey = Buffer.from(this.configuredAccount.sk).toString('base64'); } return result; } // Account operations async getAccountInfo(address) { try { const accountInfo = await this.algodClient.accountInformation(address).do(); return { address, balance: accountInfo.amount / 1000000, // Convert microAlgos to Algos minBalance: accountInfo['min-balance'] / 1000000, assets: accountInfo.assets || [], createdAssets: accountInfo['created-assets'] || [], createdApps: accountInfo['created-apps'] || [], totalAssetsOptedIn: accountInfo['total-assets-opted-in'] || 0, totalAppsOptedIn: accountInfo['total-apps-opted-in'] || 0, rewardBase: accountInfo['reward-base'], rewards: accountInfo.rewards, explorerUrls: this.getExplorerUrls('account', address) }; } catch (error) { throw new Error(`Failed to get account info: ${error.message}`); } } // Transaction operations async getTransaction(txId) { try { const transaction = await this.indexerClient.lookupTransactionByID(txId).do(); return { ...transaction.transaction, explorerUrls: this.getExplorerUrls('transaction', txId) }; } catch (error) { throw new Error(`Failed to get transaction: ${error.message}`); } } async sendPayment(from, to, amount, note = '', privateKey) { try { // If no privateKey provided, try to use configured account from env let senderAddress = from; let senderPrivateKey = privateKey; if (!privateKey) { // Check if we have a configured account with private key if (!this.configuredAccount || !this.configuredAccount.sk) { throw new Error('No private key provided and no account with private key configured in environment'); } // Use configured account if 'from' is not specified or matches configured account if (!from || from === 'default' || from === 'configured' || from === this.configuredAddress) { senderAddress = this.configuredAddress; senderPrivateKey = this.configuredAccount.sk; } else { throw new Error(`Private key required for address ${from} (configured account is ${this.configuredAddress})`); } } const params = await this.algodClient.getTransactionParams().do(); const enc = new TextEncoder(); const txn = algosdk.makePaymentTxnWithSuggestedParams( senderAddress, to, amount * 1000000, // Convert Algos to microAlgos undefined, note ? enc.encode(note) : undefined, params ); const signedTxn = txn.signTxn(senderPrivateKey); const { txId } = await this.algodClient.sendRawTransaction(signedTxn).do(); return { txId, status: 'pending', from: senderAddress, to: to, amount: amount, explorerUrls: this.getExplorerUrls('transaction', txId) }; } catch (error) { throw new Error(`Failed to send payment: ${error.message}`); } } // Block operations async getBlock(round) { try { const block = await this.algodClient.block(round).do(); return block; } catch (error) { throw new Error(`Failed to get block: ${error.message}`); } } async getCurrentBlock() { try { const status = await this.algodClient.status().do(); const block = await this.algodClient.block(status['last-round']).do(); return { round: status['last-round'], timestamp: block.block.ts, transactionsCount: block.block.txns ? block.block.txns.length : 0, rewards: block.block.rewards }; } catch (error) { throw new Error(`Failed to get current block: ${error.message}`); } } // Network status async getNetworkStatus() { try { const status = await this.algodClient.status().do(); const supply = await this.algodClient.supply().do(); return { lastRound: status['last-round'], lastBlockTime: status['time-since-last-round'], chainId: status['genesis-id'], networkVersion: status['genesis-hash'], catchupTime: status['catchup-time'], totalSupply: supply['total-money'] / 1000000, onlineSupply: supply['online-money'] / 1000000 }; } catch (error) { throw new Error(`Failed to get network status: ${error.message}`); } } // Asset operations async createAsset(creator, name, unitName, total, decimals, privateKey, metadata = {}) { try { const params = await this.algodClient.getTransactionParams().do(); const txn = algosdk.makeAssetCreateTxnWithSuggestedParams( creator, undefined, total, decimals, false, // default frozen creator, // manager creator, // reserve creator, // freeze creator, // clawback unitName, name, metadata.url || '', metadata.metadataHash || undefined, params ); const signedTxn = txn.signTxn(privateKey); const { txId } = await this.algodClient.sendRawTransaction(signedTxn).do(); return { txId, status: 'pending' }; } catch (error) { throw new Error(`Failed to create asset: ${error.message}`); } } async getAssetInfo(assetId) { try { const asset = await this.algodClient.getAssetByID(assetId).do(); return { id: assetId, name: asset.params.name, unitName: asset.params['unit-name'], total: asset.params.total, decimals: asset.params.decimals, creator: asset.params.creator, manager: asset.params.manager, reserve: asset.params.reserve, freeze: asset.params.freeze, clawback: asset.params.clawback, defaultFrozen: asset.params['default-frozen'], explorerUrls: this.getExplorerUrls('asset', assetId) }; } catch (error) { throw new Error(`Failed to get asset info: ${error.message}`); } } async getAssetBySymbol(symbol) { try { // Convert symbol to uppercase for consistency const upperSymbol = symbol.toUpperCase(); // Check if symbol exists in mapping if (!this.assetMapping[upperSymbol]) { // Return list of available symbols if not found const availableSymbols = Object.keys(this.assetMapping).sort(); throw new Error(`Unknown asset symbol: ${symbol} on ${this.network}. Available symbols: ${availableSymbols.join(', ')}`); } // Special case for ALGO if (upperSymbol === 'ALGO') { return { id: 0, symbol: 'ALGO', name: 'Algorand', unitName: 'ALGO', decimals: 6, total: 10000000000, isNative: true, network: this.network, explorerUrls: this.getExplorerUrls('asset', 0) }; } const assetId = this.assetMapping[upperSymbol]; const assetInfo = await this.getAssetInfo(assetId); return { ...assetInfo, symbol: upperSymbol, network: this.network }; } catch (error) { if (error.message.includes('Unknown asset symbol')) { throw error; } throw new Error(`Failed to get asset by symbol: ${error.message}`); } } // Get all available asset symbols getAvailableAssets() { const mainnetAssets = Object.entries(this.assetMapping) .filter(([symbol]) => !symbol.endsWith('_TESTNET')) .map(([symbol, id]) => ({ symbol, id })) .sort((a, b) => a.symbol.localeCompare(b.symbol)); const testnetAssets = Object.entries(this.assetMapping) .filter(([symbol]) => symbol.endsWith('_TESTNET')) .map(([symbol, id]) => ({ symbol: symbol.replace('_TESTNET', ''), id, network: 'testnet' })); return { mainnet: mainnetAssets, testnet: testnetAssets, total: mainnetAssets.length }; } // Get balance of a specific asset for an account async getAssetBalance(address, assetId = 0) { try { const accountInfo = await this.indexerClient.lookupAccountByID(address).do(); // If assetId is 0 or undefined, return ALGO balance if (!assetId || assetId === 0) { return { assetId: 0, symbol: 'ALGO', name: 'Algorand', balance: accountInfo.account.amount, formatted: (accountInfo.account.amount / 1000000).toFixed(6), decimals: 6, isNative: true, explorerUrls: this.getExplorerUrls('account', address) }; } // Find the specific asset in the account's holdings const asset = accountInfo.account.assets?.find(a => a['asset-id'] === assetId); if (!asset) { return { assetId: assetId, balance: 0, formatted: '0', message: 'Asset not found in account or not opted-in', explorerUrls: this.getExplorerUrls('asset', assetId) }; } // Get asset details const assetInfo = await this.getAssetInfo(assetId); const decimals = assetInfo.decimals || 0; const formattedBalance = decimals > 0 ? (asset.amount / Math.pow(10, decimals)).toFixed(decimals) : asset.amount.toString(); return { assetId: assetId, symbol: assetInfo.unitName || assetInfo.name, name: assetInfo.name, balance: asset.amount, formatted: formattedBalance, decimals: decimals, isFrozen: asset['is-frozen'] || false, explorerUrls: this.getExplorerUrls('asset', assetId) }; } catch (error) { throw new Error(`Failed to get asset balance: ${error.message}`); } } // Generate explorer URLs for various entities getExplorerUrls(type, id) { const urls = {}; switch(type) { case 'account': case 'address': urls.lora = `${this.explorer.lora}/account/${id}`; if (this.explorer.pera) { urls.pera = `${this.explorer.pera}/address/${id}`; } break; case 'asset': urls.lora = `${this.explorer.lora}/asset/${id}`; if (this.explorer.pera) { urls.pera = `${this.explorer.pera}/asset/${id}`; } break; case 'transaction': case 'tx': urls.lora = `${this.explorer.lora}/transaction/${id}`; if (this.explorer.pera) { urls.pera = `${this.explorer.pera}/tx/${id}`; } break; case 'application': case 'app': urls.lora = `${this.explorer.lora}/application/${id}`; if (this.explorer.pera) { urls.pera = `${this.explorer.pera}/application/${id}`; } break; case 'block': urls.lora = `${this.explorer.lora}/block/${id}`; if (this.explorer.pera) { urls.pera = `${this.explorer.pera}/block/${id}`; } break; } return urls; } // Smart contract operations async deployContract(approval, clear, creator, privateKey, localInts = 0, localBytes = 0, globalInts = 0, globalBytes = 0) { try { const params = await this.algodClient.getTransactionParams().do(); const txn = algosdk.makeApplicationCreateTxn( creator, params, algosdk.OnApplicationComplete.NoOpOC, approval, clear, localInts, localBytes, globalInts, globalBytes ); const signedTxn = txn.signTxn(privateKey); const { txId } = await this.algodClient.sendRawTransaction(signedTxn).do(); return { txId, status: 'pending' }; } catch (error) { throw new Error(`Failed to deploy contract: ${error.message}`); } } async callContract(appId, sender, privateKey, appArgs = [], accounts = [], foreignApps = [], foreignAssets = []) { try { const params = await this.algodClient.getTransactionParams().do(); const txn = algosdk.makeApplicationNoOpTxn( sender, params, appId, appArgs, accounts, foreignApps, foreignAssets ); const signedTxn = txn.signTxn(privateKey); const { txId } = await this.algodClient.sendRawTransaction(signedTxn).do(); return { txId, status: 'pending' }; } catch (error) { throw new Error(`Failed to call contract: ${error.message}`); } } async getContractState(appId) { try { const app = await this.algodClient.getApplicationByID(appId).do(); return { id: appId, creator: app.params.creator, approvalProgram: app.params['approval-program'], clearProgram: app.params['clear-state-program'], globalState: app.params['global-state'], globalStateSchema: app.params['global-state-schema'], localStateSchema: app.params['local-state-schema'] }; } catch (error) { throw new Error(`Failed to get contract state: ${error.message}`); } } // DeFi operations (simplified examples) async getAccountAssets(address) { try { const accountInfo = await this.algodClient.accountInformation(address).do(); const assets = []; for (const asset of accountInfo.assets || []) { const assetInfo = await this.getAssetInfo(asset['asset-id']); assets.push({ ...assetInfo, balance: asset.amount / Math.pow(10, assetInfo.decimals) }); } return assets; } catch (error) { throw new Error(`Failed to get account assets: ${error.message}`); } } // Utility functions async generateAccount() { const account = algosdk.generateAccount(); return { address: account.addr, privateKey: Buffer.from(account.sk).toString('base64'), mnemonic: algosdk.secretKeyToMnemonic(account.sk) }; } async importAccount(mnemonic) { try { const account = algosdk.mnemonicToSecretKey(mnemonic); return { address: account.addr, privateKey: Buffer.from(account.sk).toString('base64') }; } catch (error) { throw new Error(`Failed to import account: ${error.message}`); } } async waitForConfirmation(txId, timeout = 10) { const startRound = (await this.algodClient.status().do())['last-round']; let currentRound = startRound; while (currentRound < startRound + timeout) { try { const pendingInfo = await this.algodClient.pendingTransactionInformation(txId).do(); if (pendingInfo['confirmed-round'] && pendingInfo['confirmed-round'] > 0) { return pendingInfo; } currentRound++; await this.algodClient.statusAfterBlock(currentRound).do(); } catch (error) { throw new Error(`Transaction not confirmed: ${error.message}`); } } throw new Error('Transaction confirmation timeout'); } // Search and analytics async searchTransactions(filters = {}) { try { let query = this.indexerClient.searchForTransactions(); let searchAddress = filters.address; // Handle configured account for address filter if (filters.address) { if (filters.address === 'default' || filters.address === 'configured' || filters.address === 'my account') { if (this.configuredAddress) { searchAddress = this.configuredAddress; } else { throw new Error('Configured account requested but ALGORAND_ACCOUNT_ADDRESS not set in environment'); } } else if (!algosdk.isValidAddress(filters.address)) { // If invalid address, try to use configured account if (this.configuredAddress) { searchAddress = this.configuredAddress; console.log(`Invalid address provided, using configured account: ${searchAddress}`); } else { throw new Error(`Invalid Algorand address: ${filters.address}. Set ALGORAND_ACCOUNT_ADDRESS to use default account`); } } query = query.address(searchAddress); } if (filters.minAmount) query = query.currencyGreaterThan(filters.minAmount * 1000000); if (filters.maxAmount) query = query.currencyLessThan(filters.maxAmount * 1000000); if (filters.notePrefix) query = query.notePrefix(filters.notePrefix); if (filters.txType) query = query.txType(filters.txType); if (filters.assetId) query = query.assetID(filters.assetId); if (filters.limit) query = query.limit(filters.limit); const result = await query.do(); // Add explorer URLs to each transaction const transactionsWithUrls = result.transactions.map(tx => ({ ...tx, explorerUrls: this.getExplorerUrls('transaction', tx.id) })); return { addressUsed: searchAddress, transactions: transactionsWithUrls, count: result.transactions.length, searchCriteria: filters }; } catch (error) { throw new Error(`Failed to search transactions: ${error.message}`); } } async getAccountHistory(address, limit = 100) { try { let targetAddress = address; // Check if address is invalid or missing if (!address || address === 'default' || address === 'configured' || address === 'my account' || !algosdk.isValidAddress(address)) { // Try to use configured account from env if (this.configuredAddress) { targetAddress = this.configuredAddress; console.log(`Using configured account address: ${targetAddress}`); } else if (!address) { throw new Error('No address provided and no account configured. Set ALGORAND_ACCOUNT_ADDRESS in environment'); } else { throw new Error(`Invalid Algorand address format: ${address}. Set ALGORAND_ACCOUNT_ADDRESS to use default account`); } } const transactions = await this.indexerClient .searchForTransactions() .address(targetAddress) .limit(limit) .do(); // Add explorer URLs to each transaction const transactionsWithUrls = transactions.transactions.map(tx => ({ ...tx, explorerUrls: this.getExplorerUrls('transaction', tx.id) })); return { address: targetAddress, transactions: transactionsWithUrls, count: transactionsWithUrls.length, accountExplorerUrls: this.getExplorerUrls('account', targetAddress) }; } catch (error) { throw new Error(`Failed to get account history: ${error.message}`); } } // NFT operations async mintNFT(creator, name, unitName, url, privateKey, metadata = {}) { return this.createAsset( creator, name, unitName, 1, // Total supply of 1 for NFT 0, // 0 decimals for NFT privateKey, { url, ...metadata } ); } async transferNFT(nftId, from, to, privateKey) { try { const params = await this.algodClient.getTransactionParams().do(); const txn = algosdk.makeAssetTransferTxnWithSuggestedParams( from, to, undefined, undefined, 1, // Transfer 1 NFT undefined, nftId, params ); const signedTxn = txn.signTxn(privateKey); const { txId } = await this.algodClient.sendRawTransaction(signedTxn).do(); return { txId, status: 'pending' }; } catch (error) { throw new Error(`Failed to transfer NFT: ${error.message}`); } } // Staking info (simplified) async getStakingInfo(address) { try { const accountInfo = await this.getAccountInfo(address); return { address, balance: accountInfo.balance, rewards: accountInfo.rewards / 1000000, rewardBase: accountInfo.rewardBase, isParticipating: accountInfo.balance >= 0.1 // Simplified check }; } catch (error) { throw new Error(`Failed to get staking info: ${error.message}`); } } } module.exports = AlgorandAPI;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Tairon-ai/algorand-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server