Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): add sol to idb.api #3995

Merged
merged 9 commits into from
Dec 17, 2024
53 changes: 51 additions & 2 deletions src/frontend/src/lib/api/idb.api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { browser } from '$app/environment';
import { ETHEREUM_NETWORK_SYMBOL } from '$env/networks/networks.env';
import {
SOLANA_DEVNET_NETWORK_SYMBOL,
SOLANA_LOCAL_NETWORK_SYMBOL,
SOLANA_MAINNET_NETWORK_SYMBOL,
SOLANA_TESTNET_NETWORK_SYMBOL
} from '$env/networks/networks.sol.env';
import { BTC_MAINNET_SYMBOL, BTC_TESTNET_SYMBOL } from '$env/tokens/tokens.btc.env';
import type { BtcAddress, EthAddress } from '$lib/types/address';
import type { IdbBtcAddress, IdbEthAddress, SetIdbAddressParams } from '$lib/types/idb';
import type { BtcAddress, EthAddress, SolAddress } from '$lib/types/address';
import type {
IdbBtcAddress,
IdbEthAddress,
IdbSolAddress,
SetIdbAddressParams
} from '$lib/types/idb';
import type { Principal } from '@dfinity/principal';
import { isNullish } from '@dfinity/utils';
import { createStore, del, get, set, update, type UseStore } from 'idb-keyval';
Expand All @@ -16,6 +27,11 @@ const idbBtcAddressesStoreTestnet = idbAddressesStore(BTC_TESTNET_SYMBOL.toLower

const idbEthAddressesStore = idbAddressesStore(ETHEREUM_NETWORK_SYMBOL.toLowerCase());

const idbSolAddressesStoreMainnet = idbAddressesStore(SOLANA_MAINNET_NETWORK_SYMBOL.toLowerCase());
const idbSolAddressesStoreTestnet = idbAddressesStore(SOLANA_TESTNET_NETWORK_SYMBOL.toLowerCase());
const idbSolAddressesStoreDevnet = idbAddressesStore(SOLANA_DEVNET_NETWORK_SYMBOL.toLowerCase());
const idbSolAddressesStoreLocal = idbAddressesStore(SOLANA_LOCAL_NETWORK_SYMBOL.toLowerCase());

export const setIdbBtcAddressMainnet = ({
address,
principal
Expand All @@ -34,6 +50,30 @@ export const setIdbEthAddress = ({
}: SetIdbAddressParams<EthAddress>): Promise<void> =>
set(principal.toText(), address, idbEthAddressesStore);

export const setIdbSolAddressMainnet = ({
address,
principal
}: SetIdbAddressParams<SolAddress>): Promise<void> =>
set(principal.toText(), address, idbSolAddressesStoreMainnet);

export const setIdbSolAddressTestnet = ({
address,
principal
}: SetIdbAddressParams<SolAddress>): Promise<void> =>
set(principal.toText(), address, idbSolAddressesStoreTestnet);

export const setIdbSolAddressDevnet = ({
address,
principal
}: SetIdbAddressParams<SolAddress>): Promise<void> =>
set(principal.toText(), address, idbSolAddressesStoreDevnet);

export const setIdbSolAddressLocal = ({
address,
principal
}: SetIdbAddressParams<SolAddress>): Promise<void> =>
set(principal.toText(), address, idbSolAddressesStoreLocal);

const updateIdbAddressLastUsage = ({
principal,
idbAddressesStore
Expand Down Expand Up @@ -62,14 +102,23 @@ export const updateIdbBtcAddressMainnetLastUsage = (principal: Principal): Promi
export const updateIdbEthAddressLastUsage = (principal: Principal): Promise<void> =>
updateIdbAddressLastUsage({ principal, idbAddressesStore: idbEthAddressesStore });

export const updateIdbSolAddressMainnetLastUsage = (principal: Principal): Promise<void> =>
updateIdbAddressLastUsage({ principal, idbAddressesStore: idbSolAddressesStoreMainnet });

export const getIdbBtcAddressMainnet = (principal: Principal): Promise<IdbBtcAddress | undefined> =>
get(principal.toText(), idbBtcAddressesStoreMainnet);

export const getIdbEthAddress = (principal: Principal): Promise<IdbEthAddress | undefined> =>
get(principal.toText(), idbEthAddressesStore);

export const getIdbSolAddressMainnet = (principal: Principal): Promise<IdbSolAddress | undefined> =>
get(principal.toText(), idbSolAddressesStoreMainnet);

export const deleteIdbBtcAddressMainnet = (principal: Principal): Promise<void> =>
del(principal.toText(), idbBtcAddressesStoreMainnet);

export const deleteIdbEthAddress = (principal: Principal): Promise<void> =>
del(principal.toText(), idbEthAddressesStore);

export const deleteIdbSolAddressMainnet = (principal: Principal): Promise<void> =>
del(principal.toText(), idbSolAddressesStoreMainnet);
15 changes: 13 additions & 2 deletions src/frontend/src/lib/services/auth.services.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { deleteIdbBtcAddressMainnet, deleteIdbEthAddress } from '$lib/api/idb.api';
import {
deleteIdbBtcAddressMainnet,
deleteIdbEthAddress,
deleteIdbSolAddressMainnet
} from '$lib/api/idb.api';
import {
TRACK_COUNT_SIGN_IN_SUCCESS,
TRACK_SIGN_IN_CANCELLED_COUNT,
Expand Down Expand Up @@ -110,6 +114,8 @@ const emptyIdbBtcAddressMainnet = (): Promise<void> => emptyIdbAddress(deleteIdb

const emptyIdbEthAddress = (): Promise<void> => emptyIdbAddress(deleteIdbEthAddress);

const emptyIdbSolAddress = (): Promise<void> => emptyIdbAddress(deleteIdbSolAddressMainnet);

// eslint-disable-next-line require-await
const clearTestnetsOption = async () => {
testnetsStore.reset({ key: 'testnets' });
Expand All @@ -128,7 +134,12 @@ const logout = async ({
busy.start();

if (clearStorages) {
await Promise.all([emptyIdbBtcAddressMainnet(), emptyIdbEthAddress(), clearTestnetsOption()]);
await Promise.all([
emptyIdbBtcAddressMainnet(),
emptyIdbEthAddress(),
emptyIdbSolAddress(),
clearTestnetsOption()
]);
}

await authStore.signOut();
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/lib/types/idb.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Address, BtcAddress, EthAddress } from '$lib/types/address';
import type { Address, BtcAddress, EthAddress, SolAddress } from '$lib/types/address';
import type { Principal } from '@dfinity/principal';

export interface IdbAddress<T extends Address> {
Expand All @@ -11,6 +11,8 @@ export type IdbBtcAddress = IdbAddress<BtcAddress>;

export type IdbEthAddress = IdbAddress<EthAddress>;

export type IdbSolAddress = IdbAddress<SolAddress>;

export interface SetIdbAddressParams<T extends Address> {
address: IdbAddress<T>;
principal: Principal;
Expand Down
196 changes: 196 additions & 0 deletions src/frontend/src/tests/lib/api/idb.api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import {
deleteIdbBtcAddressMainnet,
deleteIdbEthAddress,
deleteIdbSolAddressMainnet,
getIdbBtcAddressMainnet,
getIdbEthAddress,
getIdbSolAddressMainnet,
setIdbBtcAddressMainnet,
setIdbEthAddress,
setIdbSolAddressMainnet,
updateIdbBtcAddressMainnetLastUsage,
updateIdbEthAddressLastUsage,
updateIdbSolAddressMainnetLastUsage
} from '$lib/api/idb.api';
import { Principal } from '@dfinity/principal';
import * as idbKeyval from 'idb-keyval';
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('idb-keyval', () => ({
createStore: vi.fn(() => ({
/* mock store implementation */
})),
set: vi.fn(),
get: vi.fn(),
del: vi.fn(),
update: vi.fn()
}));

vi.mock('$app/environment', () => ({
browser: true
}));

describe('idb.api', () => {
const mockPrincipal = Principal.fromText('2vxsx-fae');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: there is already a reusable mockPrincipal defined in src/frontend/src/tests/mocks/identity.mock.ts

const mockAddress = {
address: '0x123',
lastUsedTimestamp: Date.now(),
createdAtTimestamp: Date.now()
};

beforeEach(() => {
vi.clearAllMocks();
});

describe('BTC operations', () => {
it('should set BTC address', async () => {
await setIdbBtcAddressMainnet({
principal: mockPrincipal,
address: mockAddress
});

expect(idbKeyval.set).toHaveBeenCalledWith(
mockPrincipal.toText(),
mockAddress,
expect.any(Object)
);
});

it('should get BTC address', async () => {
vi.mocked(idbKeyval.get).mockResolvedValue(mockAddress);

const result = await getIdbBtcAddressMainnet(mockPrincipal);

expect(result).toEqual(mockAddress);
expect(idbKeyval.get).toHaveBeenCalledWith(mockPrincipal.toText(), expect.any(Object));
});

it('should delete BTC address', async () => {
await deleteIdbBtcAddressMainnet(mockPrincipal);

expect(idbKeyval.del).toHaveBeenCalledWith(mockPrincipal.toText(), expect.any(Object));
});

it('should update BTC address last usage', async () => {
// eslint-disable-next-line local-rules/prefer-object-params
vi.mocked(idbKeyval.update).mockImplementation((_, updater) => {
const updated = updater(mockAddress) as typeof mockAddress;
expect(updated.lastUsedTimestamp).toBeGreaterThan(mockAddress.lastUsedTimestamp);
return Promise.resolve();
});

await updateIdbBtcAddressMainnetLastUsage(mockPrincipal);

expect(idbKeyval.update).toHaveBeenCalled();
});
});

describe('ETH operations', () => {
it('should set ETH address', async () => {
await setIdbEthAddress({
principal: mockPrincipal,
address: mockAddress
});

expect(idbKeyval.set).toHaveBeenCalledWith(
mockPrincipal.toText(),
mockAddress,
expect.any(Object)
);
});

it('should get ETH address', async () => {
vi.mocked(idbKeyval.get).mockResolvedValue(mockAddress);

const result = await getIdbEthAddress(mockPrincipal);

expect(result).toEqual(mockAddress);
expect(idbKeyval.get).toHaveBeenCalledWith(mockPrincipal.toText(), expect.any(Object));
});

it('should delete ETH address', async () => {
await deleteIdbEthAddress(mockPrincipal);

expect(idbKeyval.del).toHaveBeenCalledWith(mockPrincipal.toText(), expect.any(Object));
});

it('should update ETH address last usage', async () => {
// eslint-disable-next-line local-rules/prefer-object-params
vi.mocked(idbKeyval.update).mockImplementation((_, updater) => {
const updated = updater(mockAddress) as typeof mockAddress;
expect(updated.lastUsedTimestamp).toBeGreaterThan(mockAddress.lastUsedTimestamp);
return Promise.resolve();
});

await updateIdbEthAddressLastUsage(mockPrincipal);

expect(idbKeyval.update).toHaveBeenCalled();
});
});

describe('SOL operations', () => {
it('should set SOL address', async () => {
await setIdbSolAddressMainnet({
principal: mockPrincipal,
address: mockAddress
});

expect(idbKeyval.set).toHaveBeenCalledWith(
mockPrincipal.toText(),
mockAddress,
expect.any(Object)
);
});

it('should get SOL address', async () => {
vi.mocked(idbKeyval.get).mockResolvedValue(mockAddress);

const result = await getIdbSolAddressMainnet(mockPrincipal);

expect(result).toEqual(mockAddress);
expect(idbKeyval.get).toHaveBeenCalledWith(mockPrincipal.toText(), expect.any(Object));
});

it('should delete SOL address', async () => {
await deleteIdbSolAddressMainnet(mockPrincipal);

expect(idbKeyval.del).toHaveBeenCalledWith(mockPrincipal.toText(), expect.any(Object));
});

it('should update SOL address last usage', async () => {
// eslint-disable-next-line local-rules/prefer-object-params
vi.mocked(idbKeyval.update).mockImplementation((_, updater) => {
const updated = updater(mockAddress) as typeof mockAddress;
expect(updated.lastUsedTimestamp).toBeGreaterThan(mockAddress.lastUsedTimestamp);
return Promise.resolve();
});

await updateIdbSolAddressMainnetLastUsage(mockPrincipal);

expect(idbKeyval.update).toHaveBeenCalled();
});
});

describe('Edge cases', () => {
it('should handle undefined address when updating last usage', async () => {
// eslint-disable-next-line local-rules/prefer-object-params
vi.mocked(idbKeyval.update).mockImplementation((_, updater) => {
const result = updater(undefined);
expect(result).toBeUndefined();
return Promise.resolve();
});

await updateIdbBtcAddressMainnetLastUsage(mockPrincipal);

expect(idbKeyval.update).toHaveBeenCalled();
});

it('should return undefined when getting non-existent address', async () => {
vi.mocked(idbKeyval.get).mockResolvedValue(undefined);

const result = await getIdbBtcAddressMainnet(mockPrincipal);

expect(result).toBeUndefined();
});
});
});
Loading