UNPKG

@ledgerhq/coin-canton

Version:
221 lines (190 loc) 8.58 kB
import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer"; import { CryptoCurrency } from "@ledgerhq/ledger-wallet-framework/types"; import { firstValueFrom, toArray } from "rxjs"; import * as signTransactionModule from "../common-logic/transaction/sign"; import * as gateway from "../network/gateway"; import { createMockCantonAccount } from "../test/fixtures"; import type { CantonSigner, CantonSignature } from "../types"; import { OnboardStatus, CantonOnboardProgress, CantonOnboardResult } from "../types/onboard"; import { buildOnboardAccount, isCantonCoinPreapproved } from "./onboard"; jest.mock("../network/gateway"); jest.mock("../common-logic/transaction/sign"); const mockedGateway = gateway as jest.Mocked<typeof gateway>; const mockedSignTransaction = signTransactionModule as jest.Mocked<typeof signTransactionModule>; describe("onboard", () => { const mockPartyId = "test-party-id"; const mockCurrency = { id: "canton_network", } as unknown as CryptoCurrency; describe("isCantonCoinPreapproved", () => { it("should return true when contract exists and is not expired", async () => { // GIVEN const futureDate = new Date(); futureDate.setHours(futureDate.getHours() + 1); mockedGateway.getTransferPreApproval.mockResolvedValue({ contract_id: "test-contract-id", receiver: mockPartyId, provider: "test-provider", valid_from: "2024-01-01T00:00:00Z", last_renewed_at: "2024-01-01T00:00:00Z", expires_at: futureDate.toISOString(), }); // WHEN const result = await isCantonCoinPreapproved(mockCurrency, mockPartyId); // THEN expect(result).toBe(true); expect(mockedGateway.getTransferPreApproval).toHaveBeenCalledWith(mockCurrency, mockPartyId); }); it("should return false when contract exists but is expired", async () => { // GIVEN const pastDate = new Date(); pastDate.setHours(pastDate.getHours() - 1); mockedGateway.getTransferPreApproval.mockResolvedValue({ contract_id: "test-contract-id", receiver: mockPartyId, provider: "test-provider", valid_from: "2024-01-01T00:00:00Z", last_renewed_at: "2024-01-01T00:00:00Z", expires_at: pastDate.toISOString(), }); // WHEN const result = await isCantonCoinPreapproved(mockCurrency, mockPartyId); // THEN expect(result).toBe(false); expect(mockedGateway.getTransferPreApproval).toHaveBeenCalledWith(mockCurrency, mockPartyId); }); }); describe("buildOnboardAccount", () => { const mockDeviceId = "test-device-id"; const mockPublicKey = "test-public-key"; const mockPartyId = "test-party-id"; const mockSignature: CantonSignature = { signature: "test-signature", }; const mockSigner: CantonSigner = { getAddress: jest.fn().mockResolvedValue({ address: "test-address", publicKey: mockPublicKey, path: "44'/6767'/0'/0/0", }), signTransaction: jest.fn().mockResolvedValue(mockSignature), } 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(); }); it("should skip submission when account is onboarded on network but has no local xpub", async () => { // GIVEN const account = createMockCantonAccount(); mockedGateway.getPartyByPubKey.mockResolvedValue({ party_id: mockPartyId, public_key: mockPublicKey, }); const onboardObservable = buildOnboardAccount(mockSignerContext); const values = await firstValueFrom( onboardObservable(mockCurrency, mockDeviceId, account).pipe(toArray()), ); // THEN const result = values.find((v): v is CantonOnboardResult => "partyId" in v); expect(result).toMatchObject({ partyId: mockPartyId, account: expect.objectContaining({ xpub: mockPartyId, cantonResources: expect.objectContaining({ isOnboarded: true }), }), }); // Should NOT call prepareOnboarding or submitOnboarding expect(mockedGateway.prepareOnboarding).not.toHaveBeenCalled(); expect(mockedGateway.submitOnboarding).not.toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).not.toHaveBeenCalled(); }); it("should proceed with submission when account has xpub (re-onboarding scenario)", async () => { // GIVEN - account already has xpub (re-onboarding) const existingPartyId = "existing-party-id"; const account = createMockCantonAccount({ xpub: existingPartyId }); const newPartyId = "new-party-id"; mockedGateway.getPartyByPubKey.mockResolvedValue({ party_id: existingPartyId, public_key: mockPublicKey, }); mockedGateway.prepareOnboarding.mockResolvedValue({ party_id: newPartyId, party_name: "test-party-name", public_key_fingerprint: "test-fingerprint", transactions: {} as any, challenge_nonce: "test-nonce", challenge_deadline: 1735689599, }); mockedGateway.submitOnboarding.mockResolvedValue({ party: { party_id: newPartyId, public_key: mockPublicKey, }, }); mockedSignTransaction.signTransaction.mockResolvedValue(mockSignature); const onboardObservable = buildOnboardAccount(mockSignerContext); const values = await firstValueFrom( onboardObservable(mockCurrency, mockDeviceId, account).pipe(toArray()), ); // THEN - should proceed through full onboarding flow const statuses = values .filter((v): v is CantonOnboardProgress => "status" in v) .map(v => v.status); expect(statuses).toContain(OnboardStatus.PREPARE); expect(statuses).toContain(OnboardStatus.SIGN); expect(statuses).toContain(OnboardStatus.SUBMIT); // Should call prepareOnboarding and submitOnboarding expect(mockedGateway.prepareOnboarding).toHaveBeenCalledWith(mockCurrency, mockPublicKey); expect(mockedGateway.submitOnboarding).toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).toHaveBeenCalled(); // Should clear topology change cache expect(mockedGateway.clearIsTopologyChangeRequiredCache).toHaveBeenCalledWith( mockCurrency, mockPublicKey, ); const result = values.find((v): v is CantonOnboardResult => "partyId" in v); expect(result).toMatchObject({ partyId: newPartyId, account: expect.objectContaining({ xpub: newPartyId, cantonResources: expect.objectContaining({ isOnboarded: true }), }), }); }); it("links the existing party (no submit) when prepareOnboarding reports it already exists", async () => { // Re-adding a just-onboarded account during the gateway's by-public-key index lag: // getPartyByPubKey still 404s, but prepareOnboarding reports the party already exists (with its id). const account = createMockCantonAccount(); const existingPartyId = "ldg::1220ac1d0368a250bc2b515a058d04a8b884cae4bb425258270ab3019b1ddd49cd1f"; mockedGateway.getPartyByPubKey.mockRejectedValue( new Error('Party with public key "pk" does not exist'), ); mockedGateway.prepareOnboarding.mockRejectedValue( new Error( `Party with id "${existingPartyId}" already exists and its topology is up to date`, ), ); mockedGateway.isPartyAlreadyExists.mockReturnValue(true); const onboardObservable = buildOnboardAccount(mockSignerContext); const values = await firstValueFrom( onboardObservable(mockCurrency, mockDeviceId, account).pipe(toArray()), ); // Linked the existing party directly — no submission, no signing. expect(mockedGateway.submitOnboarding).not.toHaveBeenCalled(); expect(mockedSignTransaction.signTransaction).not.toHaveBeenCalled(); const result = values.find((v): v is CantonOnboardResult => "partyId" in v); expect(result).toMatchObject({ partyId: existingPartyId, account: expect.objectContaining({ xpub: existingPartyId, cantonResources: expect.objectContaining({ isOnboarded: true }), }), }); }); }); });