@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
263 lines (224 loc) • 7.86 kB
text/typescript
import * as v8 from "v8";
import { BigNumber } from "bignumber.js";
import { buildTransactionPayload, encodeToSign, encodeToBroadcast } from "./buildTransaction";
import * as network from "./network";
import type { AlgorandAccount, Transaction } from "./types";
jest.mock("./network");
jest.mock("algosdk", () => {
class SignedTransaction {
sig: Uint8Array;
txn: unknown;
constructor({ sig, txn }: { sig: Uint8Array; txn: unknown }) {
this.sig = sig;
this.txn = txn;
}
}
return {
base64ToBytes: jest.fn((b64: string) => Buffer.from(b64, "base64")),
encodeMsgpack: jest.fn((obj: unknown) => v8.serialize(obj)),
SignedTransaction,
makePaymentTxnWithSuggestedParamsFromObject: jest.fn(
(params: {
sender: string;
receiver: string;
amount: number;
suggestedParams: unknown;
note?: Uint8Array;
}) => ({
sender: params.sender,
receiver: params.receiver,
amount: params.amount,
type: "pay",
suggestedParams: params.suggestedParams,
...(params.note ? { note: params.note } : {}),
}),
),
makeAssetTransferTxnWithSuggestedParamsFromObject: jest.fn(
(params: {
sender: string;
receiver: string;
amount: number;
assetIndex: number;
suggestedParams: unknown;
note?: Uint8Array;
}) => ({
sender: params.sender,
receiver: params.receiver,
amount: params.amount,
assetIndex: params.assetIndex,
type: "axfer",
suggestedParams: params.suggestedParams,
...(params.note ? { note: params.note } : {}),
}),
),
};
});
const mockGetTransactionParams = network.getTransactionParams as jest.MockedFunction<
typeof network.getTransactionParams
>;
const algosdk = jest.requireMock("algosdk");
describe("buildTransaction", () => {
const mockAccount = {
freshAddress: "ALGO_ADDRESS_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
subAccounts: [],
} as unknown as AlgorandAccount;
const defaultParams = {
fee: 0,
minFee: 1000,
firstRound: 50000000,
lastRound: 50001000,
genesisHash: "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
genesisID: "mainnet-v1.0",
};
beforeEach(() => {
jest.clearAllMocks();
mockGetTransactionParams.mockResolvedValue(defaultParams);
});
describe("buildTransactionPayload", () => {
it("should build a payment transaction payload", async () => {
const transaction: Transaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: null,
memo: undefined,
assetId: undefined,
};
const result = await buildTransactionPayload(mockAccount, transaction);
expect(result).not.toBeUndefined();
expect(result.type).toBe("pay");
expect(result.amount).toBe(1000000);
expect(result.sender).toBe(mockAccount.freshAddress);
expect(result.receiver).toBe("RECIPIENT_ADDRESS");
expect(mockGetTransactionParams).toHaveBeenCalledTimes(1);
expect(algosdk.makePaymentTxnWithSuggestedParamsFromObject).toHaveBeenCalled();
});
it("should build a transaction with memo", async () => {
const transaction: Transaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: null,
memo: "Test payment",
assetId: undefined,
};
const result = await buildTransactionPayload(mockAccount, transaction);
expect(result).not.toBeUndefined();
expect(result.note).toEqual(new TextEncoder().encode("Test payment"));
});
it("should build an asset transfer transaction for optIn", async () => {
const transaction: Transaction = {
family: "algorand",
mode: "optIn",
amount: new BigNumber("0"),
recipient: "ALGO_ADDRESS",
fees: null,
memo: undefined,
assetId: "algorand/asa/12345",
};
const result = await buildTransactionPayload(mockAccount, transaction);
expect(result).not.toBeUndefined();
expect(result.type).toBe("axfer");
expect(result.assetIndex).toBe(12345);
expect(algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject).toHaveBeenCalled();
});
it("should build payment transaction when no assetId and no subAccount", async () => {
const transaction: Transaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "ALGO_ADDRESS",
fees: null,
memo: undefined,
assetId: undefined,
};
const result = await buildTransactionPayload(mockAccount, transaction);
expect(result).not.toBeUndefined();
expect(result.type).toBe("pay");
expect(algosdk.makePaymentTxnWithSuggestedParamsFromObject).toHaveBeenCalled();
});
it("should use subAccount token for asset transfer", async () => {
const accountWithSubAccount = {
...mockAccount,
subAccounts: [
{
id: "sub-account-1",
type: "TokenAccount",
token: { id: "algorand/asa/67890" },
},
],
} as unknown as AlgorandAccount;
const transaction: Transaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("100"),
recipient: "RECIPIENT_ADDRESS",
fees: null,
memo: undefined,
subAccountId: "sub-account-1",
};
const result = await buildTransactionPayload(accountWithSubAccount, transaction);
expect(result).not.toBeUndefined();
expect(result.type).toBe("axfer");
expect(result.assetIndex).toBe(67890);
});
it("should pass genesisHash as bytes in suggestedParams", async () => {
const transaction: Transaction = {
family: "algorand",
mode: "send",
amount: new BigNumber("1000000"),
recipient: "RECIPIENT_ADDRESS",
fees: null,
memo: undefined,
assetId: undefined,
};
await buildTransactionPayload(mockAccount, transaction);
expect(algosdk.base64ToBytes).toHaveBeenCalledWith(defaultParams.genesisHash);
});
});
describe("encodeToSign", () => {
it("should encode payload to hex string", () => {
const payload = {
amt: 1000000,
fee: 1000,
type: "pay",
};
const result = encodeToSign(payload as any);
expect(typeof result).toBe("string");
expect(result).toMatch(/^[a-f0-9]+$/i);
expect(algosdk.encodeMsgpack).toHaveBeenCalledWith(payload);
});
it("should produce consistent encoding", () => {
const payload = {
amt: 500000,
type: "pay",
};
const result1 = encodeToSign(payload as any);
const result2 = encodeToSign(payload as any);
expect(result1).toBe(result2);
});
});
describe("encodeToBroadcast", () => {
it("should encode signed transaction to buffer", () => {
const payload = {
amt: 1000000,
fee: 1000,
type: "pay",
};
const signature = Buffer.alloc(66, 0xab);
const result = encodeToBroadcast(payload as any, signature);
expect(Buffer.isBuffer(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
});
it("should truncate signature to 64 bytes", () => {
const payload = { amt: 100, type: "pay" };
const signature = Buffer.alloc(66, 0xcd);
encodeToBroadcast(payload as any, signature);
const signedArg = (algosdk.encodeMsgpack as jest.Mock).mock.calls.at(-1)?.[0];
expect(signedArg).toBeInstanceOf(algosdk.SignedTransaction);
expect(signedArg.sig.length).toBe(64);
});
});
});