UNPKG

@ledgerhq/coin-canton

Version:
300 lines (269 loc) 9.12 kB
import { CoinConfig } from "@ledgerhq/coin-module-framework/config"; import { createEmptyHistoryCache, encodeAccountId, } from "@ledgerhq/ledger-wallet-framework/account"; import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer"; import prepareTransferMock from "@ledgerhq/hw-app-canton/tests/fixtures/prepare-transfer.json"; import { CryptoCurrency, CryptoCurrencyIdSchema } from "@ledgerhq/ledger-wallet-framework/types"; import type { Account } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import coinConfig, { type CantonCoinConfig } from "../config"; import type { CantonAccount, Transaction } from "../types"; import type { InstrumentBalance, OnboardingPrepareResponse, PrepareTransferResponse, TransferProposal, } from "../types/gateway"; import type { CantonPreparedTransaction, CantonSignature, CantonSigner, CantonUntypedVersionedMessage, } from "../types/signer"; import { type CantonTestKeyPair, createMockSigner as createCantonMockSigner, generateMockKeyPair, } from "./cantonTestUtils"; const DEFAULT_VALUES = { INSTRUMENT: { ADMIN_ID: "AmuletAdmin", ID: "Amulet", }, CONFIG: { GATEWAY_URL: "https://canton-gateway-devnet.api.live.ledger-test.com", NETWORK_TYPE: "devnet", }, } as const; export function createFactory<T>(defaults: T) { return (overrides: Partial<T> = {}): T => ({ ...defaults, ...overrides, }); } export const createMockCantonCurrency = (): CryptoCurrency => { const mockCurrency = { id: CryptoCurrencyIdSchema.parse("canton_network"), name: "Canton", type: "CryptoCurrency", family: "canton", units: [{ name: "Canton", code: "CANTON", magnitude: 38 }], ticker: "CANTON", scheme: "canton", color: "#000000", managerAppName: "Canton", coinType: 6767, explorerViews: [], } satisfies CryptoCurrency; return mockCurrency; }; export const createMockCantonAccount = ( overrides: Partial<Account | CantonAccount> = {}, partialCantonResources?: Partial<CantonAccount["cantonResources"]>, ) => { const currency = createMockCantonCurrency(); const derivationMode = "canton" as const; const freshAddressPath = "44'/6767'/0'/0'/0'"; const freshAddress = "alice::1220f6efa949a0dcaab8bb1a066cf0ecbca370375e90552edd6d33c14be01082b000"; const cantonResources = { publicKey: "", instrumentUtxoCounts: {}, pendingTransferProposals: [], }; const accountId = encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: freshAddress, derivationMode, }); const baseAccount = { id: accountId, type: "Account", used: true, currency, derivationMode, index: 0, freshAddress, freshAddressPath, creationDate: new Date(), lastSyncDate: new Date(), balance: new BigNumber(0), spendableBalance: new BigNumber(0), seedIdentifier: "test_seed", blockHeight: 0, operationsCount: 0, operations: [], pendingOperations: [], balanceHistoryCache: createEmptyHistoryCache(), swapHistory: [], subAccounts: [], cantonResources, ...overrides, }; const cantonResourcesFromOverrides = "cantonResources" in overrides ? overrides.cantonResources : undefined; if (cantonResourcesFromOverrides || partialCantonResources) { return { ...baseAccount, cantonResources: { ...cantonResources, ...cantonResourcesFromOverrides, ...partialCantonResources, }, } as CantonAccount; } return baseAccount as CantonAccount; }; class MockCantonSigner implements CantonSigner { private readonly keyPair: CantonTestKeyPair; private readonly mockSigner: ReturnType<typeof createCantonMockSigner>; constructor(keyPair?: CantonTestKeyPair) { this.keyPair = keyPair || generateMockKeyPair(); this.mockSigner = createCantonMockSigner(this.keyPair); } async getAddress(path: string, _display?: boolean) { const address = await this.mockSigner.getAddress(path); return { ...address, path, }; } async signTransaction( path: string, data: CantonPreparedTransaction | CantonUntypedVersionedMessage | string, ): Promise<CantonSignature> { const signatureResult = await this.mockSigner.signTransaction(path, data); if (typeof data === "object" && "transactions" in data && typeof data.challenge === "string") { const challengeHash = data.challenge.replace(/^0x/, ""); return { signature: signatureResult.signature, applicationSignature: this.keyPair.sign(challengeHash), }; } return { signature: signatureResult.signature }; } getKeyPair(): CantonTestKeyPair { return this.keyPair; } } export const createMockCantonSigner = (keyPair?: CantonTestKeyPair): MockCantonSigner => { return new MockCantonSigner(keyPair); }; export const createMockSignerContext = ( signer?: CantonSigner, keyPair?: CantonTestKeyPair, ): SignerContext<CantonSigner> => { const mockSigner = signer || createMockCantonSigner(keyPair); const mockFn = jest.fn( async (_deviceId: string, callback: (signer: CantonSigner) => Promise<CantonSignature>) => { return callback(mockSigner); }, ); return mockFn as SignerContext<CantonSigner>; }; export const createMockCantonSignature = createFactory<CantonSignature>({ signature: "test-signature", }); export const createMockTransaction = createFactory<Transaction>({ family: "canton", amount: new BigNumber(100), recipient: "alice::1220f6efa949a0dcaab8bb1a066cf0ecbca370375e90552edd6d33c14be01082b000", fee: new BigNumber(10), tokenId: "", memo: "", }); export const createMockPrepareTransferResponse = createFactory<PrepareTransferResponse>({ hash: "test-hash", json: prepareTransferMock.json, serialized: "serialized-transaction", step: { type: "single-step" }, }); const createMockOnboardingTransactions = ( overrides?: Partial<OnboardingPrepareResponse["transactions"]>, ): OnboardingPrepareResponse["transactions"] => ({ namespace_transaction: { serialized: "namespace-transaction-data", transaction: {}, hash: "namespace-hash", }, party_to_key_transaction: { serialized: "party-to-key-transaction-data", transaction: {}, hash: "party-to-key-hash", }, party_to_participant_transaction: { serialized: "party-to-participant-transaction-data", transaction: {}, hash: "party-to-participant-hash", }, combined_hash: "combined-hash", ...overrides, }); export const createMockOnboardingPrepareResponse = createFactory<OnboardingPrepareResponse>({ party_id: "test-party-id", party_name: "test-party-name", public_key_fingerprint: "test-fingerprint", transactions: createMockOnboardingTransactions(), challenge_nonce: "1234567890abcdef", challenge_deadline: 1735689599, }); const createMockInstrumentBalance = createFactory<InstrumentBalance>({ admin_id: DEFAULT_VALUES.INSTRUMENT.ADMIN_ID, instrument_id: DEFAULT_VALUES.INSTRUMENT.ID, amount: "1000", locked: false, utxo_count: 1, }); export const createMockInstrumentBalances = ( count: number = 1, overrides: Partial<InstrumentBalance> | Partial<InstrumentBalance>[] = {}, ): InstrumentBalance[] => { const overridesArray = Array.isArray(overrides) ? overrides : [overrides]; return Array.from({ length: count }, (_, index) => createMockInstrumentBalance(overridesArray[index] || overridesArray[0] || {}), ); }; export const setupMockCoinConfig = (overrides: Partial<Record<string, unknown>> = {}): void => { coinConfig.setCoinConfig( () => ({ gatewayUrl: DEFAULT_VALUES.CONFIG.GATEWAY_URL, useGateway: true, networkType: DEFAULT_VALUES.CONFIG.NETWORK_TYPE, nativeInstrumentId: DEFAULT_VALUES.INSTRUMENT.ID, status: { type: "active", }, ...overrides, }) as unknown as ReturnType<Parameters<typeof coinConfig.setCoinConfig>[0]>, ); }; export const createMockCoinConfigValue = createFactory({ gatewayUrl: DEFAULT_VALUES.CONFIG.GATEWAY_URL, useGateway: true, networkType: DEFAULT_VALUES.CONFIG.NETWORK_TYPE, nativeInstrumentId: DEFAULT_VALUES.INSTRUMENT.ID, minReserve: 100, status: { type: "active" as const }, }); export const createMockCoinConfig = (): CoinConfig<CantonCoinConfig> => { const configValue = createMockCoinConfigValue(); return jest.fn(() => configValue); }; export const createMockPendingTransferProposal = createFactory<TransferProposal>({ contract_id: "test-contract-id", sender: "alice::1220f6efa949a0dcaab8bb1a066cf0ecbca370375e90552edd6d33c14be01082b000", receiver: "bob::122014d3f17b82700d15bef451bcb1d112136eb82bb3f4fd2a4649f95a9c4632b000", instrument_admin: DEFAULT_VALUES.INSTRUMENT.ADMIN_ID, instrument_id: DEFAULT_VALUES.INSTRUMENT.ID, amount: "100", memo: "test memo", expires_at_micros: 1_800_000_000_000_000, // fixed far-future timestamp (microseconds) update_id: "test-update-id", }); export { generateMockKeyPair }; export type { CantonTestKeyPair } from "./cantonTestUtils";