Skip to content

Commit 115737f

Browse files
authored
Merge pull request LedgerHQ#11395 from LedgerHQ/LIVE-20069-add-call-to-cal-ledger-service-in-the-bridge
feat: add call to CAL in bridge
2 parents 8864d84 + 544edf6 commit 115737f

File tree

9 files changed

+275
-0
lines changed

9 files changed

+275
-0
lines changed

.changeset/orange-cows-crash.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ledgerhq/types-live": minor
3+
"@ledgerhq/live-common": minor
4+
---
5+
6+
feat: add call to CAL in bridge
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { isCALIntegrationEnabled, getCALStore } from "./cal-integration";
2+
import { CALStore } from "./cal-store";
3+
import { isFeature } from "../../featureFlags/firebaseFeatureFlags";
4+
5+
jest.mock("../../featureFlags/firebaseFeatureFlags", () => ({
6+
isFeature: jest.fn(),
7+
}));
8+
9+
const mockIsFeature = jest.mocked(isFeature);
10+
11+
describe("CAL Integration", () => {
12+
beforeEach(() => {
13+
mockIsFeature.mockClear();
14+
});
15+
16+
describe("isCALIntegrationEnabled", () => {
17+
it("should return false when feature flag is disabled", () => {
18+
mockIsFeature.mockReturnValue(false);
19+
20+
expect(isCALIntegrationEnabled()).toBe(false);
21+
expect(mockIsFeature).toHaveBeenCalledWith("calLedgerService");
22+
});
23+
24+
it("should return true when feature flag is enabled (assuming MOCK is false)", () => {
25+
mockIsFeature.mockReturnValue(true);
26+
27+
const result = isCALIntegrationEnabled();
28+
expect(mockIsFeature).toHaveBeenCalledWith("calLedgerService");
29+
expect(result).toEqual(true);
30+
});
31+
32+
it("should return false when feature check throws", () => {
33+
mockIsFeature.mockImplementation(() => {
34+
throw new Error("Feature check error");
35+
});
36+
37+
expect(isCALIntegrationEnabled()).toBe(false);
38+
});
39+
});
40+
41+
describe("getCALStore", () => {
42+
it("should return a CALStore instance", () => {
43+
const store = getCALStore();
44+
expect(store).toBeInstanceOf(CALStore);
45+
});
46+
47+
it("should return the same instance on multiple calls", () => {
48+
const store1 = getCALStore();
49+
const store2 = getCALStore();
50+
expect(store1).toBe(store2);
51+
});
52+
});
53+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getEnv } from "@ledgerhq/live-env";
2+
import { log } from "@ledgerhq/logs";
3+
import type { CryptoAssetsStore } from "@ledgerhq/types-live";
4+
import { CALStore } from "./cal-store";
5+
import { isFeature } from "../../featureFlags/firebaseFeatureFlags";
6+
7+
let calStoreInstance: CALStore | undefined;
8+
9+
export function isCALIntegrationEnabled(): boolean {
10+
try {
11+
return !getEnv("MOCK") && isFeature("calLedgerService");
12+
} catch (error) {
13+
log("cal", "Error checking CAL integration:", error);
14+
return false;
15+
}
16+
}
17+
18+
export function getCALStore(): CryptoAssetsStore {
19+
if (!calStoreInstance) {
20+
calStoreInstance = new CALStore();
21+
}
22+
return calStoreInstance;
23+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { CALStore } from "./cal-store";
2+
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
3+
4+
describe("CALStore", () => {
5+
let store: CALStore;
6+
7+
beforeEach(() => {
8+
store = new CALStore();
9+
});
10+
11+
describe("Token cache operations", () => {
12+
const token = {
13+
id: "ethereum/erc20/usdt",
14+
name: "Tether USD",
15+
ticker: "USDT",
16+
contractAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
17+
parentCurrency: { id: "ethereum" },
18+
type: "TokenCurrency",
19+
units: [
20+
{
21+
name: "USDT",
22+
code: "USDT",
23+
magnitude: 6,
24+
},
25+
],
26+
} as TokenCurrency;
27+
28+
it("should store and retrieve tokens by ID", () => {
29+
store.addTokens([token]);
30+
31+
const foundToken = store.findTokenById("ethereum/erc20/usdt");
32+
expect(foundToken).toEqual(token);
33+
});
34+
35+
it("should store and retrieve tokens by address", () => {
36+
store.addTokens([token]);
37+
38+
const foundToken = store.findTokenByAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7");
39+
expect(foundToken).toEqual(token);
40+
});
41+
42+
it("should store and retrieve tokens by ticker", () => {
43+
store.addTokens([token]);
44+
45+
const foundToken = store.findTokenByTicker("USDT");
46+
expect(foundToken).toEqual(token);
47+
});
48+
49+
it("should find token by address in specific currency", () => {
50+
store.addTokens([token]);
51+
52+
const foundToken = store.findTokenByAddressInCurrency(
53+
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
54+
"ethereum",
55+
);
56+
expect(foundToken).toEqual(token);
57+
});
58+
59+
it("should not find token by address in different currency", () => {
60+
store.addTokens([token]);
61+
62+
const foundToken = store.findTokenByAddressInCurrency(
63+
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
64+
"polygon",
65+
);
66+
expect(foundToken).toBeUndefined();
67+
});
68+
69+
it("should return undefined for non-existent address in findTokenByAddressInCurrency", () => {
70+
store.addTokens([token]);
71+
72+
const foundToken = store.findTokenByAddressInCurrency("0xNONEXISTENT", "ethereum");
73+
expect(foundToken).toBeUndefined();
74+
});
75+
76+
it("should return undefined for non-existent tokens", () => {
77+
expect(store.findTokenById("non-existent")).toBeUndefined();
78+
expect(store.findTokenByAddress("0x0000")).toBeUndefined();
79+
expect(store.findTokenByTicker("FAKE")).toBeUndefined();
80+
});
81+
82+
it("should throw error when getting non-existent token by ID", () => {
83+
expect(() => store.getTokenById("non-existent")).toThrow("Token not found: non-existent");
84+
});
85+
86+
it("should get existing token by ID", () => {
87+
store.addTokens([token]);
88+
89+
const foundToken = store.getTokenById("ethereum/erc20/usdt");
90+
expect(foundToken).toEqual(token);
91+
});
92+
});
93+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { CryptoAssetsStore } from "@ledgerhq/types-live";
2+
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
3+
4+
export class CALStore implements CryptoAssetsStore {
5+
private tokenCache = new Map<string, TokenCurrency>();
6+
private addressCache = new Map<string, TokenCurrency>();
7+
private tickerCache = new Map<string, TokenCurrency>();
8+
9+
addTokens(tokens: TokenCurrency[]) {
10+
tokens.forEach(token => {
11+
this.tokenCache.set(token.id, token);
12+
if (token.contractAddress) {
13+
this.addressCache.set(token.contractAddress, token);
14+
}
15+
this.tickerCache.set(token.ticker, token);
16+
});
17+
}
18+
19+
findTokenByAddress(address: string): TokenCurrency | undefined {
20+
return this.addressCache.get(address);
21+
}
22+
23+
getTokenById(id: string): TokenCurrency {
24+
const token = this.tokenCache.get(id);
25+
if (!token) {
26+
throw new Error(`Token not found: ${id}`);
27+
}
28+
return token;
29+
}
30+
31+
findTokenById(id: string): TokenCurrency | undefined {
32+
return this.tokenCache.get(id);
33+
}
34+
35+
findTokenByAddressInCurrency(address: string, currencyId: string): TokenCurrency | undefined {
36+
const token = this.addressCache.get(address);
37+
if (token && token.parentCurrency.id === currencyId) {
38+
return token;
39+
}
40+
return undefined;
41+
}
42+
43+
findTokenByTicker(ticker: string): TokenCurrency | undefined {
44+
return this.tickerCache.get(ticker);
45+
}
46+
}

libs/ledger-live-common/src/bridge/crypto-assets/index.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,22 @@ import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
22
import { getCryptoAssetsStore, setCryptoAssetsStore } from ".";
33
import * as legacy from "@ledgerhq/cryptoassets/tokens";
44
import type { CryptoAssetsStore } from "@ledgerhq/types-live";
5+
import * as calIntegration from "./cal-integration";
56

67
describe("Testing CryptoAssetStore", () => {
8+
let isCALIntegrationEnabledSpy: jest.SpyInstance;
9+
let getCALStoreSpy: jest.SpyInstance;
10+
11+
beforeEach(() => {
12+
isCALIntegrationEnabledSpy = jest
13+
.spyOn(calIntegration, "isCALIntegrationEnabled")
14+
.mockReturnValue(false);
15+
getCALStoreSpy = jest.spyOn(calIntegration, "getCALStore");
16+
});
17+
18+
afterEach(() => {
19+
jest.restoreAllMocks();
20+
});
721
it("should return the default methods from cryptoassets libs when feature flag does not exists", () => {
822
LiveConfig.setConfig({
923
some_other_feature: {
@@ -68,4 +82,37 @@ describe("Testing CryptoAssetStore", () => {
6882
const store = getCryptoAssetsStore();
6983
expect(store).toBe(newStore);
7084
});
85+
86+
it("should return CAL store when CAL integration is enabled", () => {
87+
isCALIntegrationEnabledSpy.mockReturnValue(true);
88+
89+
const mockCALStore = {} as unknown as CryptoAssetsStore;
90+
getCALStoreSpy.mockReturnValue(mockCALStore);
91+
92+
const store = getCryptoAssetsStore();
93+
94+
expect(isCALIntegrationEnabledSpy).toHaveBeenCalled();
95+
expect(getCALStoreSpy).toHaveBeenCalled();
96+
expect(store).toBe(mockCALStore);
97+
});
98+
99+
it("should prioritize CAL integration over feature flags", () => {
100+
isCALIntegrationEnabledSpy.mockReturnValue(true);
101+
102+
LiveConfig.setConfig({
103+
feature_cal_lazy_loading: {
104+
type: "boolean",
105+
default: true,
106+
},
107+
});
108+
109+
const mockCALStore = {} as unknown as CryptoAssetsStore;
110+
getCALStoreSpy.mockReturnValue(mockCALStore);
111+
112+
const store = getCryptoAssetsStore();
113+
114+
expect(store).toBe(mockCALStore);
115+
expect(isCALIntegrationEnabledSpy).toHaveBeenCalled();
116+
expect(getCALStoreSpy).toHaveBeenCalled();
117+
});
71118
});

libs/ledger-live-common/src/bridge/crypto-assets/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
22
import * as legacy from "@ledgerhq/cryptoassets/tokens";
33
import type { CryptoAssetsStore } from "@ledgerhq/types-live";
4+
import { isCALIntegrationEnabled, getCALStore } from "./cal-integration";
45

56
const legacyStore: CryptoAssetsStore = {
67
findTokenByAddress: legacy.findTokenByAddress,
@@ -17,6 +18,10 @@ export function setCryptoAssetsStore(store: CryptoAssetsStore) {
1718
}
1819

1920
export function getCryptoAssetsStore(): CryptoAssetsStore {
21+
if (isCALIntegrationEnabled()) {
22+
return getCALStore();
23+
}
24+
2025
const featureEnabled =
2126
LiveConfig.isConfigSet() && LiveConfig.getValueByKey("feature_cal_lazy_loading");
2227
if (!featureEnabled) {

libs/ledger-live-common/src/featureFlags/defaultFeatures.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export const DEFAULT_FEATURES: Features = {
108108
postOnboardingAssetsTransfer: DEFAULT_FEATURE,
109109
counterValue: DEFAULT_FEATURE,
110110
mockFeature: DEFAULT_FEATURE,
111+
calLedgerService: DEFAULT_FEATURE,
111112
ptxServiceCtaExchangeDrawer: DEFAULT_FEATURE,
112113
ptxServiceCtaScreens: DEFAULT_FEATURE,
113114
ptxSwapReceiveTRC20WithoutTrx: DEFAULT_FEATURE,

libs/ledgerjs/packages/types-live/src/feature.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export type Features = CurrencyFeatures & {
267267
llmSentry: DefaultFeature;
268268
onboardingIgnoredOsUpdates: Feature_OnboardingIgnoredOSUpdates;
269269
supportDeviceApex: DefaultFeature;
270+
calLedgerService: DefaultFeature;
270271
};
271272

272273
/**

0 commit comments

Comments
 (0)