@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
428 lines (365 loc) • 14.1 kB
text/typescript
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);
});
});
});