@ledgerhq/coin-casper
Version:
Ledger Casper integration
340 lines (302 loc) • 10.7 kB
text/typescript
import BigNumber from "bignumber.js";
import { getUnit, mapTxToOps } from "./txn";
import { casperAccountHashFromPublicKey, isAddressValid } from "./addresses";
import * as fixtures from "../../test/fixtures";
import { ITxnHistoryData } from "../../api/types";
import { createNewTransaction as testCreateNewTransaction } from "./txn";
// Import Casper SDK mock for direct access to mocks
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mockCasperSDK = require("casper-js-sdk");
// Mock the entire Casper SDK
jest.mock("casper-js-sdk", () => {
const mockTransaction = { id: "mock-transaction" };
const mockHelper = {
createTransferTransaction: jest.fn().mockReturnValue(mockTransaction),
};
return {
CasperNetwork: {
create: jest.fn().mockResolvedValue(mockHelper),
},
PublicKey: {
fromHex: jest.fn().mockReturnValue({ value: "mocked-public-key" }),
},
Transaction: jest.fn().mockImplementation(() => mockTransaction),
};
});
jest.mock("@ledgerhq/coin-framework/operation", () => ({
encodeOperationId: jest.fn(
(accountId: string, hash: string, type: string) => `${accountId}-${hash}-${type}`,
),
}));
jest.mock("./addresses", () => ({
casperAccountHashFromPublicKey: jest.fn((key: string) => `account-hash-${key}`),
isAddressValid: jest.fn(),
}));
jest.mock("../../api", () => ({
getCasperNodeRpcClient: jest.fn(),
}));
describe("txn", () => {
// Reset mocks between tests
beforeEach(() => {
jest.clearAllMocks();
});
describe("getUnit", () => {
test("should return the first unit of casper currency", () => {
const unit = getUnit();
expect(unit).toBeDefined();
expect(unit.name).toBe("CSPR");
expect(unit.code).toBe("CSPR");
expect(unit.magnitude).toBe(9);
});
});
describe("mapTxToOps", () => {
const { mockAccountId } = fixtures.createMockAccountShapeData();
const fees = new BigNumber("100000000");
const mockTimestamp = "2023-01-01T12:00:00Z";
beforeEach(() => {
(casperAccountHashFromPublicKey as jest.Mock).mockImplementation(
(key: string) => `account-hash-${key}`,
);
});
test("should map an outgoing transaction to operation", () => {
// Use a modified version of the fixture tx data
const fixtureData = fixtures.createMockAccountShapeData();
const txData: ITxnHistoryData = {
...fixtureData.mockTxs[0],
timestamp: mockTimestamp,
caller_public_key: "owner-public-key",
args: {
target: {
cl_type: "PublicKey",
parsed: "recipient-public-key",
},
amount: {
parsed: "5000000000",
cl_type: "U512",
},
id: {
parsed: 12345,
cl_type: {
Option: "U64",
},
},
},
deploy_hash: "test-deploy-hash",
status: "success",
amount: "5000000000",
};
// Set up the owner address hash to match the from account hash
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(
"account-hash-owner-public-key",
);
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(
"account-hash-recipient-public-key",
);
const mapper = mapTxToOps(mockAccountId, "account-hash-owner-public-key", fees);
const operations = mapper(txData);
expect(operations).toHaveLength(1);
expect(operations[0]).toEqual({
id: `${mockAccountId}-${txData.deploy_hash}-OUT`,
hash: txData.deploy_hash,
type: "OUT",
value: new BigNumber("5000000000").plus(fees),
fee: fees,
blockHeight: 1,
hasFailed: false,
blockHash: null,
accountId: mockAccountId,
senders: ["account-hash-owner-public-key"],
recipients: ["account-hash-recipient-public-key"],
date: new Date(mockTimestamp),
extra: {
transferId: "12345",
},
});
});
test("should map an incoming transaction to operation", () => {
const recipientAddressHash = "account-hash-owner-public-key";
// Use a modified version of the fixture tx data
const fixtureData = fixtures.createMockAccountShapeData();
const txData: ITxnHistoryData = {
...fixtureData.mockTxs[0],
timestamp: mockTimestamp,
caller_public_key: "sender-public-key",
args: {
target: {
cl_type: "PublicKey",
parsed: "owner-public-key",
},
amount: {
parsed: "5000000000",
cl_type: "U512",
},
id: {
parsed: 12345,
cl_type: {
Option: "U64",
},
},
},
deploy_hash: "test-deploy-hash",
status: "success",
amount: "5000000000",
};
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(
"account-hash-sender-public-key",
);
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(recipientAddressHash);
const mapper = mapTxToOps(mockAccountId, recipientAddressHash, fees);
const operations = mapper(txData);
expect(operations).toHaveLength(1);
expect(operations[0]).toEqual({
id: `${mockAccountId}-${txData.deploy_hash}-IN`,
hash: txData.deploy_hash,
type: "IN",
value: new BigNumber("5000000000"),
fee: fees,
blockHeight: 1,
hasFailed: false,
blockHash: null,
accountId: mockAccountId,
senders: ["account-hash-sender-public-key"],
recipients: [recipientAddressHash],
date: new Date(mockTimestamp),
extra: {
transferId: "12345",
},
});
});
test("should map a failed transaction correctly", () => {
// Use a modified version of the fixture tx data
const fixtureData = fixtures.createMockAccountShapeData();
const txData: ITxnHistoryData = {
...fixtureData.mockTxs[0],
timestamp: mockTimestamp,
caller_public_key: "owner-public-key",
args: {
target: {
cl_type: "PublicKey",
parsed: "recipient-public-key",
},
amount: {
parsed: "5000000000",
cl_type: "U512",
},
id: {
parsed: 12345,
cl_type: {
Option: "U64",
},
},
},
deploy_hash: "test-deploy-hash",
error_message: "Transaction failed due to insufficient funds",
status: "failure",
amount: "5000000000",
};
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(
"account-hash-owner-public-key",
);
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(
"account-hash-recipient-public-key",
);
const mapper = mapTxToOps(mockAccountId, "account-hash-owner-public-key", fees);
const operations = mapper(txData);
expect(operations).toHaveLength(1);
expect(operations[0].hasFailed).toBe(true);
});
test("should handle target as account hash string directly", () => {
// Use a modified version of the fixture tx data
const fixtureData = fixtures.createMockAccountShapeData();
const txData: ITxnHistoryData = {
...fixtureData.mockTxs[0],
timestamp: mockTimestamp,
caller_public_key: "owner-public-key",
args: {
target: {
cl_type: {
ByteArray: 32,
},
parsed: "account-hash-direct-address",
},
amount: {
parsed: "5000000000",
cl_type: "U512",
},
id: {
parsed: 12345,
cl_type: {
Option: "U64",
},
},
},
deploy_hash: "test-deploy-hash",
status: "success",
amount: "5000000000",
};
(casperAccountHashFromPublicKey as jest.Mock).mockReturnValueOnce(
"account-hash-owner-public-key",
);
const mapper = mapTxToOps(mockAccountId, "account-hash-owner-public-key", fees);
const operations = mapper(txData);
expect(operations).toHaveLength(1);
expect(operations[0].recipients).toEqual(["account-hash-direct-address"]);
});
test("should catch and log errors when mapping fails", () => {
const txData = {
timestamp: mockTimestamp,
caller_public_key: "owner-public-key",
args: {
// Intentionally missing the target field to cause an error
amount: {
parsed: "5000000000",
cl_type: "U512",
},
},
deploy_hash: "test-deploy-hash",
block_hash: "block-hash-1",
execution_type_id: 1,
cost: "100000",
payment_amount: "100000000",
status: "success",
amount: "5000000000",
};
const mapper = mapTxToOps(mockAccountId, "account-hash-owner-public-key", fees);
const operations = mapper(txData as any);
expect(operations).toEqual([]);
});
});
describe("createNewTransaction", () => {
const { mockAddress: mockSender } = fixtures.createMockAccountShapeData();
const mockRecipient = fixtures.TEST_ADDRESSES.RECIPIENT_SECP256K1;
const mockAmount = new BigNumber("5000000000");
const mockFees = new BigNumber("100000000");
const mockTransferId = fixtures.TEST_TRANSFER_IDS.VALID;
// Direct access to the mocks from jest.mock
const mockCreateNetwork = mockCasperSDK.CasperNetwork.create;
const mockPublicKeyFromHex = mockCasperSDK.PublicKey.fromHex;
beforeEach(() => {
(isAddressValid as jest.Mock).mockReturnValue(true);
});
test("should throw error if recipient address is invalid", async () => {
(isAddressValid as jest.Mock).mockReturnValue(false);
await expect(
testCreateNewTransaction(mockSender, mockRecipient, mockAmount, mockFees),
).rejects.toThrow();
});
test("should create a new transaction with valid parameters", async () => {
const expectedTransaction = { id: "mock-transaction" };
const result = await testCreateNewTransaction(
mockSender,
mockRecipient,
mockAmount,
mockFees,
mockTransferId,
);
expect(result).toEqual(expectedTransaction);
expect(mockCreateNetwork).toHaveBeenCalled();
expect(mockPublicKeyFromHex).toHaveBeenCalledWith(mockSender);
expect(mockPublicKeyFromHex).toHaveBeenCalledWith(mockRecipient);
});
});
});