@ledgerhq/coin-canton
Version:
344 lines (309 loc) • 11.1 kB
text/typescript
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();
});
});
});