@ledgerhq/coin-mina
Version:
226 lines (196 loc) • 8.28 kB
text/typescript
import {
getAccountNumFromPath,
isValidAddress,
getMaxAmount,
getTotalSpent,
reEncodeRawSignature,
isValidMemo,
} from ".";
import { BigNumber } from "bignumber.js";
import type { MinaAccount, Transaction } from "../types/common";
import { MAX_MEMO_LENGTH } from "../consts";
import { Operation } from "@ledgerhq/types-live";
// Create a minimal mock implementation for the tests
type MockMinaAccount = Pick<MinaAccount, "spendableBalance" | "pendingOperations">;
type MockTransaction = Partial<Transaction>;
// Mock functions to create test objects
function createMockAccount(overrides: Partial<MockMinaAccount> = {}): MockMinaAccount {
return {
spendableBalance: new BigNumber(1000),
pendingOperations: [],
...overrides,
};
}
function createMockTransaction(overrides: Partial<MockTransaction> = {}): MockTransaction {
return {
family: "mina",
amount: new BigNumber(0),
recipient: "B62qiVhtBtqakq8sNTHdCTXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc",
...overrides,
};
}
describe("getAccountNumFromPath", () => {
it("should return undefined for invalid account number", () => {
const account = getAccountNumFromPath("44'/616'/4'/0/0");
expect(account).toBe(undefined);
});
it("should return undefined for unsupported path", () => {
const account = getAccountNumFromPath("44'/616'/4/0/0");
expect(account).toBe(undefined);
});
it("should return the account number", () => {
const account = getAccountNumFromPath("44'/12586'/4'/0/0");
expect(account).toBe(4);
});
});
describe("testingAddress", () => {
const validAddresses = [
"B62qiVhtBtqakq8sNTHdCTXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc",
"B62qr5cXFjdnZXYxP5dEwRY7wENxcod4Q2oLxUDiq1QrBXZZyxMH8q4",
"B62qqmRToRiXZjyLLMJoGnQFXXZuZWdqaNYr43xEDDQzkXMHLouCMZp",
"B62qnixP59E4vJytpDWZ5YQPrnmqb7YNwhVWiuSAkxbYdX5RauHWwcm",
"B62qmfBiJx88gM9opov4MDxmDVKMutQsBQ1aGt7TrQYyGQx66fRFxvC",
"B62qosg21UpwpfPFaxTyirdfUVDHe2Ynsyv1QydE4z34zbLuBZ92ALM",
"B62qkWUvsVEnXmd4poHbkKujHNnjxkDcZtrLxN1FTa8pp3GkfHMB1gv",
"B62qoiK7DJurVUUJx1dmJGULdNUmX5bjdiYvoWXEXGugS2Cx3YDsHhy",
"B62qn1KptFUxxu8aRS52NGQ3GWzn5h8v7U9k571r4LCs5obpoZyd81e",
"B62qis2tX1rveddCeJeBAZJnXcCwWPp1x9ScZ3iMCxbV29gRbxWo7qi",
"B62qpBKEDTnS4efjBmou7iztx5x152JEmc29XMR1Y3FVCpnmfKGX4R4",
"B62qro5bGnk7eCNMA1S5QWmzn5sCcRWckbw83GGaqN3HM2QzP9zYS83",
"B62qospDjUj43x2yMKiNehojWWRUsE1wpdUDVpfxH8V3n5Y1QgJKFfw",
"B62qrmYwToypQv8v9cjxJdyhAxKihrqtTtEkYC4Y3u412KCPLGSD93V",
"B62qj3rpYkGM1T3VN6JDAJs9NccHCRQDYMcQ6UCBc81ff5HXTDjU44d",
"B62qp2kks3r14P8GWkR2ieT18JobwnmrBFGEoqy3dAReiyM8aDp4be7",
"B62qpW1SYPcp7TcHJYL6egnN9qzvNtQL5XGyiM8S6UsHvxCYzhhFPjz",
"B62qospDjUj43x2yMKiNehojWWRUsE1wpdUDVpfxH8V3n5Y1QgJKFfw",
"B62qj5L8U8A8gexpHLWHxBdfjKuWb8HMguVPecQ5FGqgZAwBAKHvhEy",
"B62qnt5WShcbVVnVqQqWjkyMJdxMwLq4QbRbgMjoi5nmvQw7dZLRtpx",
"B62qrGUxqRWn9A8PEJschgLZuS5S1GJiairh65mgTapaK58nkTKHUs8",
"B62qkS7hmMkzmNDjeCUKyAMZSw1c98MxszZPA7WnvBuh7Pq66ztiwdt",
"B62qjGWerE9MPNk7E2jEJe9Ex5GxkNR9JCc5mhFeJdGFaNDHnMdGQJQ",
"B62qpp2DLkLxwFn1QZdkEWgiRFMYnkgh6cj5AJC5pqCYiALh8BmiYJ8",
"B62qqALaAoEnaJPSNmQaDkUwFGiKwZo4zRK4Ay5eSMxAMVhNxwr2KeM",
"B62qospDjUj43x2yMKiNehojWWRUsE1wpdUDVpfxH8V3n5Y1QgJKFfw",
"B62qr3epzHUrxrdminUN4Hzuinuc7aBDAFSR94mPmmxVsbuFpUvDDq6",
"B62qrJurUKtcqdhQu1XtJJfSH7rR1xrEtSWPQQcFwBJBwVcsFSyVSM3",
];
const invalidAddresses = [
"", // Empty address
null, // Null address
undefined, // Undefined address
"123", // Too short
"B61qiVhtBtqakq8sNTHdCTXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc", // Incorrect prefix
"B62q!@#$%^&*()_+{}|:<>?`~", // Invalid characters
"B62qj5L8U8A8gexpHLWHxBdfjKuWb8HMguVPecQ5FGqgZAwBAKHvhE", // Too short
"B62qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", // Too long
" B62qiVhtBtqakq8sNTHdCTXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc", // Leading whitespace
"B62qiVhtBtqakq8sNTHdCTXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc ", // Trailing whitespace
"B62qiVhtBtqakq8sNTHdC TXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc", // Internal whitespace
"C62qiVhtBtqakq8sNTHdCTXn6tETSK6gtsmNHRn1WdLqjGLpsHbw1xc", // Incorrect prefix
];
it("should return true for valid address", () => {
for (const address of validAddresses) {
expect(isValidAddress(address)).toBe(true);
}
});
it("should return false for invalid address", () => {
for (const address of invalidAddresses) {
expect(isValidAddress(address as string)).toBe(false);
}
});
});
describe("isValidMemo", () => {
it("should return true for valid memo", () => {
expect(isValidMemo("")).toBe(true);
expect(isValidMemo("Valid memo")).toBe(true);
expect(isValidMemo("A".repeat(MAX_MEMO_LENGTH))).toBe(true);
});
it("should return false for memo exceeding max length", () => {
expect(isValidMemo("A".repeat(MAX_MEMO_LENGTH + 1))).toBe(false);
});
});
describe("getMaxAmount", () => {
it("should calculate correct max amount with no fees", () => {
const account = createMockAccount({
spendableBalance: new BigNumber(1000),
pendingOperations: [],
});
const transaction = createMockTransaction();
expect(getMaxAmount(account as MinaAccount, transaction as Transaction).toNumber()).toBe(1000);
});
it("should calculate correct max amount with fees", () => {
const account = createMockAccount({
spendableBalance: new BigNumber(1000),
pendingOperations: [],
});
const transaction = createMockTransaction();
const fees = new BigNumber(10);
expect(getMaxAmount(account as MinaAccount, transaction as Transaction, fees).toNumber()).toBe(
990,
);
});
it("should subtract pending operations from max amount", () => {
const account = createMockAccount({
spendableBalance: new BigNumber(1000),
pendingOperations: [
{ value: new BigNumber(100) } as Operation,
{ value: new BigNumber(200) } as Operation,
],
});
const transaction = createMockTransaction();
expect(getMaxAmount(account as MinaAccount, transaction as Transaction).toNumber()).toBe(700);
});
it("should return 0 when max amount would be negative", () => {
const account = createMockAccount({
spendableBalance: new BigNumber(100),
pendingOperations: [{ value: new BigNumber(200) } as Operation],
});
const transaction = createMockTransaction();
expect(getMaxAmount(account as MinaAccount, transaction as Transaction).toNumber()).toBe(0);
});
});
describe("getTotalSpent", () => {
it("should return spendable balance when useAllAmount is true", () => {
const account = createMockAccount({
spendableBalance: new BigNumber(1000),
});
const transaction = createMockTransaction({
useAllAmount: true,
amount: new BigNumber(500),
});
const fees = new BigNumber(10);
expect(getTotalSpent(account as MinaAccount, transaction as Transaction, fees).toNumber()).toBe(
1000,
);
});
it("should return amount plus fees when useAllAmount is false", () => {
const account = createMockAccount({
spendableBalance: new BigNumber(1000),
});
const transaction = createMockTransaction({
useAllAmount: false,
amount: new BigNumber(500),
});
const fees = new BigNumber(10);
expect(getTotalSpent(account as MinaAccount, transaction as Transaction, fees).toNumber()).toBe(
510,
);
});
});
describe("reEncodeRawSignature", () => {
it("should correctly re-encode a valid signature", () => {
const rawSignature = "a".repeat(64) + "b".repeat(64);
const result = reEncodeRawSignature(rawSignature);
// Check that the result has the correct format (reverse bytes)
expect(result.length).toBe(128);
// Check field part
const fieldPart = result.substring(0, 64);
expect(fieldPart).toBe("a".repeat(64).match(/.{2}/g)?.reverse().join(""));
// Check scalar part
const scalarPart = result.substring(64);
expect(scalarPart).toBe("b".repeat(64).match(/.{2}/g)?.reverse().join(""));
});
it("should throw an error for invalid signature length", () => {
expect(() => reEncodeRawSignature("too_short")).toThrow("Invalid raw signature input");
});
});