UNPKG

@zebec-network/exchange-card-sdk

Version:
195 lines (194 loc) 8.71 kB
import algosdk from "algosdk"; import { ALGORAND_RPC_URL } from "../constants"; import { ZebecCardAPIService } from "../helpers/apiHelpers"; import { formatAlgo, formatAlgorandAsset, getAssetDecimals, parseAlgo, parseAlgorandAsset, } from "../utils"; export class AlgorandService { wallet; algodClient; apiService; constructor(wallet, sdkOptions) { this.wallet = wallet; const rpcUrl = ALGORAND_RPC_URL[sdkOptions?.sandbox ? "Sandbox" : "Production"]; this.algodClient = new algosdk.Algodv2({}, rpcUrl, 443); this.apiService = new ZebecCardAPIService(sdkOptions?.sandbox || false); } /** * Fetches the Bitcoin vault address. * * @returns {Promise<{ address: string }>} A promise that resolves to the vault address. */ async fetchVault(symbol = "ALGO") { const data = await this.apiService.fetchVault(symbol); return data; } /** * Transfer Algorand currency from one wallet to another * @param config Transfer configuration * @returns Transaction ID if successful */ async transferAlgo(config) { try { const parsedAmount = parseAlgo(config.amount); // Check if sender has sufficient balance const senderBalance = await this.getAccountBalanceInMicroAlgo(this.wallet.address); const minBalance = parseAlgo(0.1); // Minimum account balance if (senderBalance < parsedAmount + minBalance) { throw new Error(`Insufficient balance. Need ${formatAlgo(parsedAmount + minBalance)} ALGO, have ${formatAlgo(senderBalance)} ALGO`); } const vault = await this.fetchVault("ALGO"); const recipientAddress = vault.address; // Validate recipient address if (!algosdk.isValidAddress(recipientAddress)) { throw new Error("Invalid recipient address"); } // Get suggested transaction parameters const suggestedParams = await this.algodClient.getTransactionParams().do(); // Create payment transaction const paymentTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ sender: this.wallet.address, receiver: recipientAddress, amount: parsedAmount, note: config.note ? new Uint8Array(Buffer.from(config.note)) : undefined, suggestedParams: suggestedParams, }); // Sign the transaction const txId = await this.wallet.signAndSendTransaction(paymentTxn); return txId; } catch (error) { console.error("Transfer failed:", error); throw error; } } /** * Transfer USDC (ASA token) from wallet to vault * @param config USDC transfer configuration * @returns Transaction ID if successful */ async transferAsset(config) { try { const assetDecimals = await getAssetDecimals(this.algodClient, config.assetId); // const usdcConfig = USDC_ASSET_CONFIG[this.network]; const parsedAmount = parseAlgorandAsset(config.amount, assetDecimals); // Check if sender has sufficient USDC balance const senderAssetBalance = await this.getAssetBalanceInMicroUnit(this.wallet.address, config.assetId); if (senderAssetBalance < parsedAmount) { throw new Error(`Insufficient Asset balance. Need ${formatAlgorandAsset(parsedAmount, assetDecimals)} Asset, have ${formatAlgorandAsset(senderAssetBalance, assetDecimals)} Asset`); } // Check if sender has sufficient ALGO for transaction fees const senderAlgoBalance = await this.getAccountBalanceInMicroAlgo(this.wallet.address); const minAlgoForFees = parseAlgo(0.002); // Minimum ALGO for transaction fees if (senderAlgoBalance < minAlgoForFees) { throw new Error(`Insufficient ALGO for transaction fees. Need at least ${formatAlgo(minAlgoForFees)} ALGO for fees`); } const vault = await this.fetchVault("ALGO-USDC"); const recipientAddress = vault.address; // Validate recipient address if (!algosdk.isValidAddress(recipientAddress)) { throw new Error("Invalid recipient address"); } // // Check if recipient is opted into USDC asset const recipientOptedIn = await this.isOptedIntoAsset(recipientAddress, config.assetId); if (!recipientOptedIn) { throw new Error("Recipient address is not opted into USDC asset"); } // Get suggested transaction parameters const suggestedParams = await this.algodClient.getTransactionParams().do(); // Create asset transfer transaction const assetTransferTxn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ sender: this.wallet.address, receiver: recipientAddress, amount: parsedAmount, assetIndex: config.assetId, note: config.note ? new Uint8Array(Buffer.from(config.note)) : undefined, suggestedParams: suggestedParams, }); // Sign and send the transaction const txId = await this.wallet.signAndSendTransaction(assetTransferTxn); return txId; } catch (error) { console.error("Asset transfer failed:", error); throw error; } } /** * Check if an account is opted into a specific asset * @param address Account address * @param assetId Asset ID to check * @returns Whether the account is opted into the asset */ async isOptedIntoAsset(address, assetId) { try { const accountInfo = await this.algodClient.accountInformation(address).do(); return accountInfo.assets?.some((asset) => asset.assetId === BigInt(assetId)) || false; } catch (error) { console.error("Error checking asset opt-in status:", error); return false; } } /** * Get asset balance for a specific account in microAsset (base units) * @param address Account address * @param assetId Asset ID * @returns Asset balance in base units */ async getAssetBalanceInMicroUnit(walletAddress, assetId) { try { const accountInfo = await this.algodClient.accountInformation(walletAddress).do(); const asset = accountInfo.assets?.find((asset) => asset.assetId === BigInt(assetId)); return asset ? BigInt(asset.amount) : BigInt(0); } catch (error) { console.error("Error fetching asset balance:", error); return BigInt(0); } } async getAssetBalance(walletAddress, assetId) { const balance = await this.getAssetBalanceInMicroUnit(walletAddress, assetId); const decimals = await getAssetDecimals(this.algodClient, assetId); return formatAlgorandAsset(balance, decimals); } async getAssetsBalance(walletAddress, assetIds) { const map = new Map(Array.from(assetIds.map((id) => [id, "0"]))); try { const accountInfo = await this.algodClient.accountInformation(walletAddress).do(); const assets = accountInfo.assets; if (!assets) { return map; } await Promise.all(assetIds.map(async (id) => { const asset = assets.find((asset) => asset.assetId === BigInt(id)); if (asset) { const decimals = await getAssetDecimals(this.algodClient, id); const amount = formatAlgorandAsset(asset.amount, decimals); map.set(id, amount); } })); return map; } catch (error) { console.error("Error fetching asset balance:", error); } return map; } /** * Get account balance in Algos * @param address Account address * @returns Balance in ALGO */ async getAccountBalance(address) { const amount = await this.getAccountBalanceInMicroAlgo(address); return formatAlgo(amount); } /** * Get account balance in microAlgos (for internal calculations) * @param address Account address * @returns Balance in microAlgos */ async getAccountBalanceInMicroAlgo(address) { const accountInfo = await this.algodClient.accountInformation(address).do(); return accountInfo.amount; } }