UNPKG

@ledgerhq/coin-canton

Version:
344 lines (309 loc) 11.1 kB
import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer"; import type { CryptoCurrency } from "@ledgerhq/ledger-wallet-framework/types"; import type { Account } from "@ledgerhq/types-live"; import * as signTransactionModule from "../common-logic/transaction/sign"; import type { PrepareTransferResponse } from "../network/gateway"; import * as gateway from "../network/gateway"; import type { CantonAccount } from "../types"; import { TopologyChangeError } from "../types/errors"; import type { CantonSignature, CantonSigner } from "../types/signer"; import { buildTransferInstruction } from "./acceptOffer"; import * as getTransactionStatusModule from "./getTransactionStatus"; jest.mock("../network/gateway"); jest.mock("../common-logic/transaction/sign"); jest.mock("./getTransactionStatus"); const mockedGateway = gateway as jest.Mocked<typeof gateway>; const mockedSignTransaction = signTransactionModule as jest.Mocked<typeof signTransactionModule>; const mockedGetTransactionStatus = getTransactionStatusModule as jest.Mocked< typeof getTransactionStatusModule >; describe("acceptOffer", () => { const mockCurrency = { id: "canton_network", } as unknown as CryptoCurrency; const mockAccount = { id: "test-account-id", freshAddressPath: "44'/6767'/0'/0'/0'", } as unknown as Account; const mockPartyId = "test-party-id"; const mockContractId = "test-contract-id"; const mockDeviceId = "test-device-id"; const mockPreparedTransaction: PrepareTransferResponse = { hash: "test-hash", json: { transaction: { version: "2.1", roots: ["0"], nodes: [ { nodeId: "0", v1: { create: { lfVersion: "2.1", contractId: mockContractId, packageName: "test-package", templateId: { packageId: "test-package-id", moduleName: "TestModule", entityName: "TestEntity", }, argument: { record: { recordId: { packageId: "test-package-id", moduleName: "TestModule", entityName: "TestEntity", }, fields: [], }, }, }, }, }, ], }, metadata: { submitterInfo: { actAs: [mockPartyId], commandId: "test-command-id", }, synchronizerId: "test-synchronizer-id", transactionUuid: "test-transaction-uuid", submissionTime: "1234567890", inputContracts: [], }, }, serialized: "serialized-transaction", step: { type: "single-step" as const }, }; const mockSignature: CantonSignature = { signature: "test-signature", }; const mockSigner: CantonSigner = { getAddress: jest.fn(), signTransaction: jest.fn(), } as unknown as CantonSigner; const mockSignerContext: SignerContext<CantonSigner> = jest.fn( async (_deviceId: string, callback: (signer: CantonSigner) => Promise<CantonSignature>) => { return callback(mockSigner); }, ) as unknown as SignerContext<CantonSigner>; beforeEach(() => { jest.clearAllMocks(); mockedGateway.prepareTransferInstruction.mockResolvedValue(mockPreparedTransaction); mockedSignTransaction.signTransaction.mockResolvedValue(mockSignature); mockedGateway.submitTransferInstruction.mockResolvedValue({ submission_id: "test-submission-id", update_id: "test-update-id", }); mockedGetTransactionStatus.validateTopology.mockResolvedValue(null); }); describe("buildTransferInstruction", () => { it("should accept transfer instruction without reason", async () => { // GIVEN const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN await transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "accept-transfer-instruction", ); // THEN expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalledWith( mockCurrency, mockPartyId, { type: "accept-transfer-instruction", contract_id: mockContractId, }, ); expect(mockedSignTransaction.signTransaction).toHaveBeenCalledWith( mockSigner, mockAccount.freshAddressPath, mockPreparedTransaction, ); expect(mockedGateway.submitTransferInstruction).toHaveBeenCalledWith( mockCurrency, mockPartyId, mockPreparedTransaction.serialized, mockSignature.signature, ); expect(mockSignerContext).toHaveBeenCalledWith(mockDeviceId, expect.any(Function)); }); it("should accept transfer instruction with reason", async () => { // GIVEN const reason = "test-reason"; const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN await transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "accept-transfer-instruction", reason, ); // THEN expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalledWith( mockCurrency, mockPartyId, { type: "accept-transfer-instruction", contract_id: mockContractId, reason, }, ); expect(mockedSignTransaction.signTransaction).toHaveBeenCalledWith( mockSigner, mockAccount.freshAddressPath, mockPreparedTransaction, ); expect(mockedGateway.submitTransferInstruction).toHaveBeenCalledWith( mockCurrency, mockPartyId, mockPreparedTransaction.serialized, mockSignature.signature, ); }); it("should reject transfer instruction", async () => { // GIVEN const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN await transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "reject-transfer-instruction", ); // THEN expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalledWith( mockCurrency, mockPartyId, { type: "reject-transfer-instruction", contract_id: mockContractId, }, ); expect(mockedSignTransaction.signTransaction).toHaveBeenCalled(); expect(mockedGateway.submitTransferInstruction).toHaveBeenCalled(); }); it("should withdraw transfer instruction", async () => { // GIVEN const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN await transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "withdraw-transfer-instruction", ); // THEN expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalledWith( mockCurrency, mockPartyId, { type: "withdraw-transfer-instruction", contract_id: mockContractId, }, ); expect(mockedSignTransaction.signTransaction).toHaveBeenCalled(); expect(mockedGateway.submitTransferInstruction).toHaveBeenCalled(); }); it("should handle errors from prepareTransferInstruction", async () => { // GIVEN const error = new Error("Prepare failed"); mockedGateway.prepareTransferInstruction.mockRejectedValue(error); const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN & THEN await expect( transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "accept-transfer-instruction", ), ).rejects.toThrow("Prepare failed"); expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).not.toHaveBeenCalled(); expect(mockedGateway.submitTransferInstruction).not.toHaveBeenCalled(); }); it("should handle errors from signTransaction", async () => { // GIVEN const error = new Error("Sign failed"); mockedSignTransaction.signTransaction.mockRejectedValue(error); const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN & THEN await expect( transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "accept-transfer-instruction", ), ).rejects.toThrow("Sign failed"); expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).toHaveBeenCalled(); expect(mockedGateway.submitTransferInstruction).not.toHaveBeenCalled(); }); it("should handle errors from submitTransferInstruction", async () => { // GIVEN const error = new Error("Submit failed"); mockedGateway.submitTransferInstruction.mockRejectedValue(error); const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN & THEN await expect( transferInstruction( mockCurrency, mockDeviceId, mockAccount, mockPartyId, mockContractId, "accept-transfer-instruction", ), ).rejects.toThrow("Submit failed"); expect(mockedGateway.prepareTransferInstruction).toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).toHaveBeenCalled(); expect(mockedGateway.submitTransferInstruction).toHaveBeenCalled(); }); it("should throw TopologyChangeError when validateTopology returns topology error", async () => { // GIVEN const topologyError = new TopologyChangeError("Topology change detected"); const cantonAccount = { ...mockAccount, cantonResources: { publicKey: "test-public-key", instrumentUtxoCounts: {}, }, } as unknown as CantonAccount; mockedGetTransactionStatus.validateTopology.mockResolvedValue(topologyError); const transferInstruction = buildTransferInstruction(mockSignerContext); // WHEN & THEN await expect( transferInstruction( mockCurrency, mockDeviceId, cantonAccount, mockPartyId, mockContractId, "accept-transfer-instruction", ), ).rejects.toThrow(TopologyChangeError); expect(mockedGetTransactionStatus.validateTopology).toHaveBeenCalledWith(cantonAccount); expect(mockedGateway.prepareTransferInstruction).not.toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).not.toHaveBeenCalled(); expect(mockedGateway.submitTransferInstruction).not.toHaveBeenCalled(); }); }); });