@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
368 lines (308 loc) • 11.9 kB
text/typescript
import { FeeNotLoaded } from "@ledgerhq/errors";
import type { TokenAccount } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import { buildOptimisticOperation } from "./buildOptimisticOperation";
import type { AlgorandAccount, AlgorandTransaction } from "./types";
jest.mock("@ledgerhq/ledger-wallet-framework/operation", () => ({
encodeOperationId: jest.fn((accountId, hash, type) => `${accountId}--${type}`),
}));
describe("buildOptimisticOperation", () => {
const createMockAccount = (balance: string, subAccounts: TokenAccount[] = []): AlgorandAccount =>
({
id: "algorand-account-1",
freshAddress: "ALGO_SENDER_ADDRESS",
spendableBalance: new BigNumber(balance),
balance: new BigNumber(balance),
algorandResources: {
rewards: new BigNumber("0"),
nbAssets: subAccounts.length,
},
subAccounts,
}) as unknown as AlgorandAccount;
describe("basic operation creation", () => {
it("should create operation with correct senders and recipients", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.senders).toEqual(["ALGO_SENDER_ADDRESS"]);
expect(result.recipients).toEqual(["RECIPIENT_ADDRESS"]);
});
it("should set correct fee", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("2500"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.fee.toString()).toBe("2500");
});
it("should set accountId from account", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.accountId).toBe("algorand-account-1");
});
it("should set date to current time", () => {
const before = new Date();
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
const after = new Date();
expect(result.date.getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(result.date.getTime()).toBeLessThanOrEqual(after.getTime());
});
it("should have empty hash", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.hash).toBe("");
});
it("should have null blockHash and blockHeight", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.blockHash).toBeNull();
expect(result.blockHeight).toBeNull();
});
});
describe("operation type", () => {
it("should be OUT for send mode", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.type).toBe("OUT");
});
it("should be OPT_IN for optIn mode", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "optIn",
amount: new BigNumber("0"),
recipient: "ALGO_SENDER_ADDRESS",
fees: new BigNumber("1000"),
assetId: "12345",
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.type).toBe("OPT_IN");
});
it("should be FEES for subAccount transaction", () => {
const tokenAccount = {
id: "token-account-1",
type: "TokenAccount",
balance: new BigNumber("1000"),
token: { id: "algorand/asa/12345" },
} as unknown as TokenAccount;
const account = createMockAccount("10000000", [tokenAccount]);
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("100"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
subAccountId: "token-account-1",
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.type).toBe("FEES");
});
});
describe("operation value", () => {
it("should be amount + fees for regular send", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.value.toString()).toBe("1001000");
});
it("should be spendableBalance for useAllAmount", () => {
const account = createMockAccount("5000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("0"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: true,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.value.toString()).toBe("5000000");
});
it("should be fees only for subAccount transaction", () => {
const tokenAccount = {
id: "token-account-1",
type: "TokenAccount",
balance: new BigNumber("500"),
token: { id: "algorand/asa/12345" },
} as unknown as TokenAccount;
const account = createMockAccount("10000000", [tokenAccount]);
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("100"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("2000"),
subAccountId: "token-account-1",
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.value.toString()).toBe("2000"); // Only fees
});
});
describe("subOperations", () => {
it("should include subOperation for token transfer", () => {
const tokenAccount = {
id: "token-account-1",
type: "TokenAccount",
balance: new BigNumber("1000"),
token: { id: "algorand/asa/12345" },
} as unknown as TokenAccount;
const account = createMockAccount("10000000", [tokenAccount]);
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("500"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
subAccountId: "token-account-1",
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.subOperations).toHaveLength(1);
expect(result.subOperations?.[0].type).toBe("OUT");
expect(result.subOperations?.[0].value.toString()).toBe("500");
expect(result.subOperations?.[0].accountId).toBe("token-account-1");
});
it("should use token balance for useAllAmount", () => {
const tokenAccount = {
id: "token-account-2",
type: "TokenAccount",
balance: new BigNumber("750"),
token: { id: "algorand/asa/67890" },
} as unknown as TokenAccount;
const account = createMockAccount("10000000", [tokenAccount]);
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("0"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
subAccountId: "token-account-2",
useAllAmount: true,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.subOperations?.[0].value.toString()).toBe("750");
});
it("should have zero fee for subOperation", () => {
const tokenAccount = {
id: "token-account-1",
type: "TokenAccount",
balance: new BigNumber("1000"),
token: { id: "algorand/asa/12345" },
} as unknown as TokenAccount;
const account = createMockAccount("10000000", [tokenAccount]);
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("500"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
subAccountId: "token-account-1",
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.subOperations?.[0].fee.toString()).toBe("0");
});
it("should not include subOperations for regular send", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: new BigNumber("1000"),
useAllAmount: false,
};
const result = buildOptimisticOperation(account, transaction);
expect(result.subOperations).toBeUndefined();
});
});
describe("error handling", () => {
it("should throw FeeNotLoaded when fees are null", () => {
const account = createMockAccount("10000000");
const transaction: AlgorandTransaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: null,
useAllAmount: false,
};
expect(() => buildOptimisticOperation(account, transaction)).toThrow(FeeNotLoaded);
});
it("should throw FeeNotLoaded when fees are undefined", () => {
const account = createMockAccount("10000000");
const transaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: undefined,
useAllAmount: false,
} as unknown as AlgorandTransaction;
expect(() => buildOptimisticOperation(account, transaction)).toThrow(FeeNotLoaded);
});
});
});