import { createApi } from "."; describe("Algorand Api (mainnet)", () => { // Algorand Foundation address - a well-known address with transaction history const SENDER = "737777777777777777777777777777777777777777777777777UFEJ2CI"; const api = createApi({ node: "https://algorand.coin.ledger.com/ps2/v2", indexer: "https://algorand.coin.ledger.com/idx2/v2", }); describe("getBalance", () => { it("returns a balance for an existing address", async () => { // When const result = await api.getBalance(SENDER); // Then expect(result.length).toBeGreaterThanOrEqual(1); expect(result[0].asset).toEqual({ type: "native" }); expect(result[0].value).toBeGreaterThanOrEqual(0n); expect(result[0].locked).toBeGreaterThanOrEqual(0n); }); it("returns balance with locked amount (minimum balance)", async () => { // When const result = await api.getBalance(SENDER); // Then // Algorand requires minimum balance of 0.1 ALGO (100000 microAlgos) expect(result[0].locked).toBeGreaterThanOrEqual(100000n); }); }); describe("lastBlock", () => { it("returns last block info", async () => { // When const result = await api.lastBlock(); // Then expect(result.height).toBeGreaterThan(0); expect(typeof result.hash).toBe("string"); expect(result.time).toBeInstanceOf(Date); }); }); describe("getBlockInfo", () => { it("returns block info for a specific height", async () => { // Given - Get the current block height first const lastBlockInfo = await api.lastBlock(); const targetHeight = lastBlockInfo.height - 10; // Get a block from 10 rounds ago // When const result = await api.getBlockInfo(targetHeight); // Then expect(result.height).toBe(targetHeight); expect(typeof result.hash).toBe("string"); expect(result.time).toBeInstanceOf(Date); expect(result.time.getTime()).toBeGreaterThan(0); }); }); describe("estimateFees", () => { it("returns estimated fees", async () => { // When const result = await api.estimateFees({ intentType: "transaction", asset: { type: "native" }, type: "send", sender: SENDER, amount: 1000000n, recipient: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ", }); // Then // Algorand minimum fee is 1000 microAlgos expect(result.value).toBeGreaterThanOrEqual(1000n); }); it("returns estimated fees for ASA token transfer", async () => { // Given - USDC on Algorand mainnet const USDC_ASSET_ID = "31566704"; const RECIPIENT = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; // When const result = await api.estimateFees({ intentType: "transaction", asset: { type: "asa", assetReference: USDC_ASSET_ID }, type: "send", sender: SENDER, amount: 1000000n, // 1 USDC recipient: RECIPIENT, }); // Then // ASA transfers also have minimum fee of 1000 microAlgos // Fee should be exactly 1000 for standard ASA transfers expect(result.value).toBeGreaterThanOrEqual(1000n); }); }); describe("listOperations", () => { it("returns operations for an address", async () => { // When const { items, next } = await api.listOperations(SENDER, { minHeight: 0, order: "desc", }); // Then expect(items.length).toBeGreaterThan(0); expect(typeof next).toBe("string"); // Verify operation structure const op = items[0]; expect(op.id).not.toBeUndefined(); expect(op.type).toMatch(/^(IN|OUT|OPT_IN|OPT_OUT)$/); expect(op.value).toBeGreaterThanOrEqual(0n); expect(op.asset).not.toBeUndefined(); expect(op.senders).toBeInstanceOf(Array); expect(op.recipients).toBeInstanceOf(Array); expect(op.tx.hash).toEqual(expect.any(String)); expect(op.tx.block.height).toBeGreaterThan(0); expect(op.tx.fees).toBeGreaterThanOrEqual(0n); expect(op.tx.date).toBeInstanceOf(Date); }); it("returns operations in ascending order when specified", async () => { // When const { items } = await api.listOperations(SENDER, { minHeight: 0, order: "asc", }); // Then if (items.length > 1) { // Check ascending order by block height for (let i = 1; i < items.length; i++) { expect(items[i].tx.block.height).toBeGreaterThanOrEqual(items[i - 1].tx.block.height); } } }); it("paginates across at least two pages", async () => { // Given - use a small limit to force pagination const limit = 5; // When - fetch first page const { items: firstPageOps, next: firstToken } = await api.listOperations(SENDER, { minHeight: 0, limit, order: "asc", }); // Then - first page should have results and a cursor for the next page expect(firstPageOps.length).toBeGreaterThan(0); expect(firstPageOps.length).toBeLessThanOrEqual(limit); expect(firstToken).not.toBe(""); // When - fetch second page using the cursor const { items: secondPageOps, next: secondToken } = await api.listOperations(SENDER, { minHeight: 0, limit, order: "asc", cursor: firstToken, }); // Then - second page should also have results expect(secondPageOps.length).toBeGreaterThan(0); expect(secondPageOps.length).toBeLessThanOrEqual(limit); // Verify no overlap between pages (operation ids should be distinct) const firstPageIds = new Set(firstPageOps.map(op => op.id)); for (const op of secondPageOps) { expect(firstPageIds.has(op.id)).toBe(false); } // Second page cursor should differ from the first (more pages or empty when done) expect(secondToken).not.toBe(firstToken); }); }); describe("craftTransaction", () => { // Zero address - always valid for crafting (though transaction will fail on broadcast) const RECIPIENT = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; it("returns a crafted transaction for native ALGO transfer", async () => { // When const { transaction, details } = await api.craftTransaction({ intentType: "transaction", asset: { type: "native" }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 1000000n, // 1 ALGO }); // Then expect(transaction.length).toBeGreaterThan(0); // Transaction should be hex encoded expect(transaction).toMatch(/^[0-9a-f]+$/i); expect(details).not.toBeUndefined(); }); it("uses custom fees when provided", async () => { // Given const customFees = 2000n; // When const { transaction } = await api.craftTransaction( { intentType: "transaction", asset: { type: "native" }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 1000000n, }, { value: customFees }, ); // Then expect(transaction.length).toBeGreaterThan(0); }); }); describe("craftTransaction ASA tokens", () => { // Zero address - always valid for crafting const RECIPIENT = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; // USDC on Algorand mainnet - Asset ID 31566704 const USDC_ASSET_ID = "31566704"; // Another well-known token: Tether USDt - Asset ID 312769 const USDT_ASSET_ID = "312769"; it("returns a crafted transaction for ASA token transfer", async () => { // When const { transaction, details } = await api.craftTransaction({ intentType: "transaction", asset: { type: "asa", assetReference: USDC_ASSET_ID }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 1000000n, // 1 USDC (6 decimals) }); // Then expect(transaction.length).toBeGreaterThan(0); // Transaction should be hex encoded expect(transaction).toMatch(/^[0-9a-f]+$/i); expect(details).not.toBeUndefined(); // ASA transactions should have asset transfer specific fields expect(details.txPayload).not.toBeUndefined(); }); it("crafts ASA transfer with custom fees", async () => { // Given const customFees = 2000n; // When const { transaction, details } = await api.craftTransaction( { intentType: "transaction", asset: { type: "asa", assetReference: USDC_ASSET_ID }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 500000n, }, { value: customFees }, ); // Then expect(transaction.length).toBeGreaterThan(0); expect(details.txPayload).not.toBeUndefined(); }); it("estimates fees then crafts ASA transaction with estimated fees", async () => { // Given const transactionIntent = { intentType: "transaction" as const, asset: { type: "asa" as const, assetReference: USDC_ASSET_ID }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 250000n, // 0.25 USDC }; // When - First estimate fees const feeEstimate = await api.estimateFees(transactionIntent); // Then - Verify fee estimate is reasonable expect(feeEstimate.value).toBeGreaterThanOrEqual(1000n); // When - Use estimated fees to craft transaction const { transaction, details } = await api.craftTransaction(transactionIntent, feeEstimate); // Then - Verify transaction was crafted expect(transaction.length).toBeGreaterThan(0); expect(transaction).toMatch(/^[0-9a-f]+$/i); expect(details).not.toBeUndefined(); expect(details.txPayload).not.toBeUndefined(); }); it("crafts ASA transfer with zero amount (opt-in style)", async () => { // When - sending 0 amount to self is how opt-in works const { transaction, details } = await api.craftTransaction({ intentType: "transaction", asset: { type: "asa", assetReference: USDT_ASSET_ID }, type: "send", sender: SENDER, recipient: SENDER, // Self-transfer amount: 0n, }); // Then expect(transaction.length).toBeGreaterThan(0); expect(transaction).toMatch(/^[0-9a-f]+$/i); expect(details.txPayload).not.toBeUndefined(); }); it("crafts ASA transfer with memo", async () => { // When const { transaction, details } = await api.craftTransaction({ intentType: "transaction", asset: { type: "asa", assetReference: USDC_ASSET_ID }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 100000n, memo: { type: "string", kind: "note", value: "ASA transfer test" }, }); // Then expect(transaction).not.toBeUndefined(); expect(transaction.length).toBeGreaterThan(0); expect(details.txPayload).not.toBeUndefined(); }); it("crafts different ASA tokens with different asset IDs", async () => { // When - craft USDC transfer const { transaction: usdcTx } = await api.craftTransaction({ intentType: "transaction", asset: { type: "asa", assetReference: USDC_ASSET_ID }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 1000n, }); // When - craft USDT transfer const { transaction: usdtTx } = await api.craftTransaction({ intentType: "transaction", asset: { type: "asa", assetReference: USDT_ASSET_ID }, type: "send", sender: SENDER, recipient: RECIPIENT, amount: 1000n, }); // Then - transactions should be different (different asset IDs) expect(usdcTx).not.toBe(usdtTx); expect(usdcTx.length).toBeGreaterThan(0); expect(usdtTx.length).toBeGreaterThan(0); }); }); describe("unsupported methods", () => { it("getBlock throws not supported error", () => { expect(() => api.getBlock(100)).toThrow("getBlock is not supported for Algorand"); }); it("getNextSequence throws not applicable error", () => { expect(() => api.getNextSequence(SENDER)).toThrow( "getNextSequence is not applicable for Algorand", ); }); it("getStakes throws not supported error", () => { expect(() => api.getStakes(SENDER)).toThrow("getStakes is not supported for Algorand"); }); it("getRewards throws not supported error", () => { expect(() => api.getRewards(SENDER)).toThrow("getRewards is not supported for Algorand"); }); it("getValidators throws not supported error", () => { expect(() => api.getValidators()).toThrow("getValidators is not supported for Algorand"); }); }); }); describe("Algorand Api (testnet)", () => { // Testnet endpoints from Algonode const api = createApi({ node: "https://testnet-api.algonode.cloud/v2", indexer: "https://testnet-idx.algonode.cloud/v2", }); // Zero address - valid for testing const TESTNET_ADDRESS = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; describe("lastBlock", () => { it("returns last block info from testnet", async () => { // When const result = await api.lastBlock(); // Then expect(result.height).toBeGreaterThan(0); }); }); describe("estimateFees", () => { it("returns minimum fee on testnet", async () => { // When const result = await api.estimateFees({ intentType: "transaction", asset: { type: "native" }, type: "send", sender: TESTNET_ADDRESS, amount: 1000000n, recipient: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ", }); // Then expect(result.value).toBeGreaterThanOrEqual(1000n); }); }); });