@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
307 lines (260 loc) • 9.07 kB
text/typescript
import * as v8 from "v8";
import type { TransactionIntent } from "@ledgerhq/coin-module-framework/api/index";
import * as network from "../network";
import type { AlgorandMemo } from "../types";
import { craftTransaction, craftOptInTransaction, craftApiTransaction } from "./craftTransaction";
jest.mock("../network");
jest.mock("algosdk", () => ({
base64ToBytes: jest.fn((s: string) => Buffer.from(s, "base64")),
encodeMsgpack: jest.fn((obj: unknown) => v8.serialize(obj)),
makePaymentTxnWithSuggestedParamsFromObject: jest.fn(
({
sender,
receiver,
amount,
note,
}: {
sender: string;
receiver: string;
amount: number;
note?: Uint8Array;
suggestedParams: unknown;
}) => ({
amt: amount,
fee: 1000,
fv: 1000,
lv: 2000,
snd: Buffer.from(sender),
rcv: Buffer.from(receiver),
type: "pay",
...(note ? { note: Buffer.from(note) } : {}),
}),
),
makeAssetTransferTxnWithSuggestedParamsFromObject: jest.fn(
({
sender,
receiver,
amount,
assetIndex,
note,
}: {
sender: string;
receiver: string;
amount: number;
assetIndex: number;
note?: Uint8Array;
suggestedParams: unknown;
}) => ({
amt: amount,
fee: 1000,
fv: 1000,
lv: 2000,
snd: Buffer.from(sender),
arcv: Buffer.from(receiver),
xaid: assetIndex,
type: "axfer",
...(note ? { note: Buffer.from(note) } : {}),
}),
),
}));
const mockGetTransactionParams = network.getTransactionParams as jest.MockedFunction<
typeof network.getTransactionParams
>;
function decodeTxPayload(serializedTransaction: string) {
return v8.deserialize(Buffer.from(serializedTransaction, "hex")) as Record<string, unknown>;
}
describe("craftTransaction", () => {
const defaultParams = {
fee: 0,
minFee: 1000,
firstRound: 1000,
lastRound: 2000,
genesisHash: "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
genesisID: "mainnet-v1.0",
};
beforeEach(() => {
jest.clearAllMocks();
mockGetTransactionParams.mockResolvedValue(defaultParams);
});
it("should craft a native ALGO payment with correct amount, fee and type", async () => {
const input = {
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
};
const result = await craftTransaction(input);
expect(result.serializedTransaction).toMatch(/^[a-f0-9]+$/i);
const decoded = decodeTxPayload(result.serializedTransaction);
expect(decoded.type).toBe("pay");
expect(decoded.amt).toBe(1000000);
expect(decoded.fee).toBe(1000);
expect(Buffer.from(decoded.snd as Uint8Array).toString()).toBe("SENDER_ADDR");
expect(Buffer.from(decoded.rcv as Uint8Array).toString()).toBe("RECIPIENT_ADDR");
});
it("should craft an ASA transfer with correct assetId and amount", async () => {
const input = {
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 100n,
assetId: "12345",
};
const result = await craftTransaction(input);
const decoded = decodeTxPayload(result.serializedTransaction);
expect(decoded.type).toBe("axfer");
expect(decoded.amt).toBe(100);
expect(decoded.xaid).toBe(12345);
expect(Buffer.from(decoded.snd as Uint8Array).toString()).toBe("SENDER_ADDR");
expect(Buffer.from(decoded.arcv as Uint8Array).toString()).toBe("RECIPIENT_ADDR");
});
it("should include memo as note in the payload", async () => {
const input = {
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
memo: "Test payment",
};
const result = await craftTransaction(input);
const decoded = decodeTxPayload(result.serializedTransaction);
expect(Buffer.from(decoded.note as Uint8Array).toString()).toBe("Test payment");
});
it("should omit note when no memo is provided", async () => {
const input = {
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
};
const result = await craftTransaction(input);
const decoded = decodeTxPayload(result.serializedTransaction);
expect(decoded.note).toBeUndefined();
});
it("should also expose txPayload with matching attributes", async () => {
const input = {
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 500000n,
};
const result = await craftTransaction(input);
expect(result.txPayload.amt).toBe(500000);
expect(result.txPayload.type).toBe("pay");
expect(result.txPayload.fee).toBe(1000);
});
it("should fetch transaction params from network", async () => {
await craftTransaction({
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
});
expect(mockGetTransactionParams).toHaveBeenCalledTimes(1);
});
});
describe("craftOptInTransaction", () => {
const defaultParams = {
fee: 0,
minFee: 1000,
firstRound: 1000,
lastRound: 2000,
genesisHash: "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
genesisID: "mainnet-v1.0",
};
beforeEach(() => {
jest.clearAllMocks();
mockGetTransactionParams.mockResolvedValue(defaultParams);
});
it("should craft an opt-in with 0 amount and assetId", async () => {
const sender = "SENDER_ADDR";
const assetId = "12345";
const result = await craftOptInTransaction(sender, assetId);
const decoded = decodeTxPayload(result.serializedTransaction);
expect(decoded.type).toBe("axfer");
expect(decoded.amt).toBe(0);
expect(decoded.xaid).toBe(12345);
expect(Buffer.from(decoded.snd as Uint8Array).toString()).toBe(sender);
expect(Buffer.from(decoded.arcv as Uint8Array).toString()).toBe(sender);
});
});
describe("craftApiTransaction", () => {
const defaultParams = {
fee: 0,
minFee: 1000,
firstRound: 1000,
lastRound: 2000,
genesisHash: "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
genesisID: "mainnet-v1.0",
};
beforeEach(() => {
jest.clearAllMocks();
mockGetTransactionParams.mockResolvedValue(defaultParams);
});
it("should craft a native ALGO transaction matching the intent", async () => {
const intent: TransactionIntent<AlgorandMemo> = {
intentType: "transaction",
type: "send",
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
asset: { type: "native" },
};
const result = await craftApiTransaction(intent);
expect(result.transaction).toMatch(/^[a-f0-9]+$/i);
const decoded = decodeTxPayload(result.transaction);
expect(decoded.type).toBe("pay");
expect(decoded.amt).toBe(1000000);
expect(Buffer.from(decoded.snd as Uint8Array).toString()).toBe(intent.sender);
expect(Buffer.from(decoded.rcv as Uint8Array).toString()).toBe(intent.recipient);
});
it("should craft an ASA transaction matching the intent", async () => {
const intent: TransactionIntent<AlgorandMemo> = {
intentType: "transaction",
type: "send",
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 500n,
asset: { type: "asa", assetReference: "12345" },
};
const result = await craftApiTransaction(intent);
const decoded = decodeTxPayload(result.transaction);
expect(decoded.type).toBe("axfer");
expect(decoded.amt).toBe(500);
expect(decoded.xaid).toBe(12345);
expect(Buffer.from(decoded.snd as Uint8Array).toString()).toBe(intent.sender);
expect(Buffer.from(decoded.arcv as Uint8Array).toString()).toBe(intent.recipient);
});
it("should include memo from intent in the payload", async () => {
const intent: TransactionIntent<AlgorandMemo> = {
intentType: "transaction",
type: "send",
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
asset: { type: "native" },
memo: { type: "string", kind: "note", value: "hello" },
};
const result = await craftApiTransaction(intent);
const decoded = decodeTxPayload(result.transaction);
expect(Buffer.from(decoded.note as Uint8Array).toString()).toBe("hello");
});
it("should expose txPayload in details", async () => {
const intent: TransactionIntent<AlgorandMemo> = {
intentType: "transaction",
type: "send",
sender: "SENDER_ADDR",
recipient: "RECIPIENT_ADDR",
amount: 1000000n,
asset: { type: "native" },
};
const result = await craftApiTransaction(intent);
expect(result.details).toHaveProperty("txPayload");
expect((result.details as { txPayload: { type: string } }).txPayload.type).toBe("pay");
});
it("should throw for non-send transaction intents", async () => {
const intent = {
intentType: "other",
type: "stake",
sender: "SENDER",
amount: 1000000n,
} as unknown as TransactionIntent<AlgorandMemo>;
await expect(craftApiTransaction(intent)).rejects.toThrow(
"Only send transaction intent is supported",
);
});
});