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;