Skip to content

Commit

Permalink
feat(frontend): add sol to idb.api (#3995)
Browse files Browse the repository at this point in the history
# Motivation

Add Solana address in the IDB functions. Use it in the auth.service.

# Changes

- Add solana
- Add tests for all networks
- Add clean up in auth.service for sol

# Tests

Unit testing added

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
loki344 and github-actions[bot] authored Dec 17, 2024
1 parent 335bb06 commit 90d529b
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 5 deletions.
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
195 changes: 195 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,195 @@
import {
deleteIdbBtcAddressMainnet,
deleteIdbEthAddress,
deleteIdbSolAddressMainnet,
getIdbBtcAddressMainnet,
getIdbEthAddress,
getIdbSolAddressMainnet,
setIdbBtcAddressMainnet,
setIdbEthAddress,
setIdbSolAddressMainnet,
updateIdbBtcAddressMainnetLastUsage,
updateIdbEthAddressLastUsage,
updateIdbSolAddressMainnetLastUsage
} from '$lib/api/idb.api';
import { mockPrincipal } from '$tests/mocks/identity.mock';
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 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();
});
});
});

0 comments on commit 90d529b

Please sign in to comment.