Skip to content

Commit

Permalink
feat(frontend): sol-address-services (#3998)
Browse files Browse the repository at this point in the history
# Motivation

Implement sol-address-service (inspired from btc-address.services.ts).

# Changes

- implement sol-address-service
- add typings
- add @solana/addresses npm dependency

# Tests

Unit tests added.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
loki344 and github-actions[bot] authored Dec 17, 2024
1 parent 4453a6f commit 405d36c
Showing 7 changed files with 490 additions and 14 deletions.
131 changes: 130 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@
"@dfinity/verifiable-credentials": "^0.0.4",
"@junobuild/analytics": "^0.0.31",
"@metamask/detect-provider": "^2.0.0",
"@solana/addresses": "^2.0.0",
"@walletconnect/web3wallet": "1.14.0",
"alchemy-sdk": "3.4.1",
"buffer": "^6.0.3",
14 changes: 14 additions & 0 deletions src/frontend/src/sol/schema/network.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NetworkSchema } from '$lib/schema/network.schema';
import { UrlSchema } from '$lib/validation/url.validation';
import { z } from 'zod';

const SolRpcSchema = z.object({
httpUrl: UrlSchema,
wssUrl: UrlSchema
});

export const SolNetworkSchema = z
.object({
rpc: SolRpcSchema
})
.merge(NetworkSchema);
157 changes: 157 additions & 0 deletions src/frontend/src/sol/services/sol-address.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
SOLANA_DEVNET_NETWORK_ID,
SOLANA_KEY_ID,
SOLANA_LOCAL_NETWORK_ID,
SOLANA_MAINNET_NETWORK_ID
} from '$env/networks/networks.sol.env';
import { SOLANA_TOKEN_ID } from '$env/tokens/tokens.sol.env';
import {
getIdbSolAddressMainnet,
setIdbSolAddressDevnet,
setIdbSolAddressLocal,
setIdbSolAddressMainnet,
setIdbSolAddressTestnet,
updateIdbSolAddressMainnetLastUsage
} from '$lib/api/idb.api';
import { getSchnorrPublicKey } from '$lib/api/signer.api';
import {
certifyAddress,
loadIdbTokenAddress,
loadTokenAddress,
validateAddress,
type LoadTokenAddressParams
} from '$lib/services/address.services';
import {
solAddressDevnetStore,
solAddressLocalnetStore,
solAddressMainnetStore,
solAddressTestnetStore,
type StorageAddressData
} from '$lib/stores/address.store';
import type { SolAddress } from '$lib/types/address';
import type { CanisterApiFunctionParams } from '$lib/types/canister';
import { LoadIdbAddressError } from '$lib/types/errors';
import type { OptionIdentity } from '$lib/types/identity';
import type { TokenId } from '$lib/types/token';
import type { ResultSuccess } from '$lib/types/utils';
import { SOLANA_DERIVATION_PATH_PREFIX } from '$sol/constants/sol.constants';
import { SolanaNetworks } from '$sol/types/network';
import { getAddressDecoder } from '@solana/addresses';

const getSolanaPublicKey = async (
params: CanisterApiFunctionParams<{ derivationPath: string[] }>
): Promise<Uint8Array | number[]> =>
await getSchnorrPublicKey({
...params,
keyId: SOLANA_KEY_ID,
derivationPath: [SOLANA_DERIVATION_PATH_PREFIX, ...params.derivationPath]
});

const getSolAddress = async ({
identity,
derivationPath
}: {
identity: OptionIdentity;
derivationPath: string[];
}): Promise<SolAddress> => {
const publicKey = await getSolanaPublicKey({ identity, derivationPath });
const decoder = getAddressDecoder();
return decoder.decode(Uint8Array.from(publicKey));
};

export const getSolAddressMainnet = async (identity: OptionIdentity): Promise<SolAddress> =>
await getSolAddress({ identity, derivationPath: [SolanaNetworks.MAINNET] });

export const getSolAddressTestnet = async (identity: OptionIdentity): Promise<SolAddress> =>
await getSolAddress({ identity, derivationPath: [SolanaNetworks.TESTNET] });

export const getSolAddressDevnet = async (identity: OptionIdentity): Promise<SolAddress> =>
await getSolAddress({ identity, derivationPath: [SolanaNetworks.DEVNET] });

export const getSolAddressLocal = async (identity: OptionIdentity): Promise<SolAddress> =>
await getSolAddress({ identity, derivationPath: [SolanaNetworks.LOCAL] });

const solanaMapper: Record<
SolanaNetworks,
Pick<LoadTokenAddressParams<SolAddress>, 'addressStore' | 'setIdbAddress' | 'getAddress'>
> = {
mainnet: {
addressStore: solAddressMainnetStore,
getAddress: getSolAddressMainnet,
setIdbAddress: setIdbSolAddressMainnet
},
testnet: {
addressStore: solAddressTestnetStore,
getAddress: getSolAddressTestnet,
setIdbAddress: setIdbSolAddressTestnet
},
devnet: {
addressStore: solAddressDevnetStore,
getAddress: getSolAddressDevnet,
setIdbAddress: setIdbSolAddressDevnet
},
local: {
addressStore: solAddressLocalnetStore,
getAddress: getSolAddressLocal,
setIdbAddress: setIdbSolAddressLocal
}
};

const loadSolAddress = ({
tokenId,
network
}: {
tokenId: TokenId;
network: SolanaNetworks;
}): Promise<ResultSuccess> =>
loadTokenAddress<SolAddress>({
tokenId,
...solanaMapper[network]
});

export const loadSolAddressMainnet = (): Promise<ResultSuccess> =>
loadSolAddress({
tokenId: SOLANA_MAINNET_NETWORK_ID as unknown as TokenId,
network: SolanaNetworks.MAINNET
});

export const loadSolAddressTestnet = (): Promise<ResultSuccess> =>
loadSolAddress({
tokenId: SOLANA_MAINNET_NETWORK_ID as unknown as TokenId,
network: SolanaNetworks.TESTNET
});

export const loadSolAddressDevnet = (): Promise<ResultSuccess> =>
loadSolAddress({
tokenId: SOLANA_DEVNET_NETWORK_ID as unknown as TokenId,
network: SolanaNetworks.DEVNET
});

export const loadSolAddressLocal = (): Promise<ResultSuccess> =>
loadSolAddress({
tokenId: SOLANA_LOCAL_NETWORK_ID as unknown as TokenId,
network: SolanaNetworks.LOCAL
});

export const loadIdbSolAddressMainnet = (): Promise<ResultSuccess<LoadIdbAddressError>> =>
loadIdbTokenAddress<SolAddress>({
tokenId: SOLANA_TOKEN_ID,
getIdbAddress: getIdbSolAddressMainnet,
updateIdbAddressLastUsage: updateIdbSolAddressMainnetLastUsage,
addressStore: solAddressMainnetStore
});

const certifySolAddressMainnet = (address: SolAddress): Promise<ResultSuccess<string>> =>
certifyAddress<SolAddress>({
tokenId: SOLANA_TOKEN_ID,
address,
getAddress: (identity: OptionIdentity) => getSolAddressMainnet(identity),
updateIdbAddressLastUsage: updateIdbSolAddressMainnetLastUsage,
addressStore: solAddressMainnetStore
});

export const validateSolAddressMainnet = async ($addressStore: StorageAddressData<SolAddress>) =>
await validateAddress<SolAddress>({
$addressStore,
certifyAddress: certifySolAddressMainnet
});
13 changes: 0 additions & 13 deletions src/frontend/src/sol/services/sol-balance.services.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/frontend/src/sol/types/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { SolNetworkSchema } from '$sol/schema/network.schema';
import { z } from 'zod';

export type SolNetwork = z.infer<typeof SolNetworkSchema>;

export enum SolanaNetworks {
MAINNET = 'mainnet',
TESTNET = 'testnet',
DEVNET = 'devnet',
LOCAL = 'local'
}
177 changes: 177 additions & 0 deletions src/frontend/src/tests/sol/services/sol-address.services.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { SOLANA_KEY_ID } from '$env/networks/networks.sol.env';
import { SOLANA_TOKEN_ID } from '$env/tokens/tokens.sol.env';
import * as idbApi from '$lib/api/idb.api';
import * as signerApi from '$lib/api/signer.api';
import {
solAddressDevnetStore,
solAddressLocalnetStore,
solAddressMainnetStore,
solAddressTestnetStore
} from '$lib/stores/address.store';
import { authStore } from '$lib/stores/auth.store';
import * as toastsStore from '$lib/stores/toasts.store';
import { LoadIdbAddressError } from '$lib/types/errors';
import { replacePlaceholders } from '$lib/utils/i18n.utils';
import { SOLANA_DERIVATION_PATH_PREFIX } from '$sol/constants/sol.constants';
import {
getSolAddressDevnet,
getSolAddressLocal,
getSolAddressMainnet,
getSolAddressTestnet,
loadIdbSolAddressMainnet,
loadSolAddressDevnet,
loadSolAddressLocal,
loadSolAddressMainnet,
loadSolAddressTestnet,
validateSolAddressMainnet
} from '$sol/services/sol-address.services';
import { SolanaNetworks } from '$sol/types/network';
import en from '$tests/mocks/i18n.mock';
import { mockIdentity } from '$tests/mocks/identity.mock';
import { getAddressDecoder } from '@solana/addresses';
import { get } from 'svelte/store';
import type { MockInstance } from 'vitest';

vi.mock('@solana/addresses', () => ({
getAddressDecoder: vi.fn()
}));

describe('sol-address.services', () => {
const mockSolAddress = 'solana123';
const mockPublicKey = new Uint8Array([1, 2, 3]);

let spyGetSchnorrPublicKey: MockInstance;
let spyGetIdbAddress: MockInstance;
let spyUpdateIdbAddressLastUsage: MockInstance;
let spyToastsError: MockInstance;
let mockDecoder: { decode: MockInstance; read: MockInstance };

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

authStore.setForTesting(mockIdentity);

mockDecoder = {
decode: vi.fn().mockReturnValue(mockSolAddress),
read: vi.fn().mockReturnValue(mockSolAddress)
};
vi.mocked(getAddressDecoder).mockReturnValue(mockDecoder as never);

spyGetSchnorrPublicKey = vi.spyOn(signerApi, 'getSchnorrPublicKey');
spyGetIdbAddress = vi.spyOn(idbApi, 'getIdbSolAddressMainnet');
spyUpdateIdbAddressLastUsage = vi.spyOn(idbApi, 'updateIdbSolAddressMainnetLastUsage');
spyToastsError = vi.spyOn(toastsStore, 'toastsError');
});

describe('Generate Solana Addresses for Different Networks', () => {
beforeEach(() => {
spyGetSchnorrPublicKey.mockResolvedValue(mockPublicKey);
});

const networkCases = [
['mainnet', getSolAddressMainnet, SolanaNetworks.MAINNET],
['testnet', getSolAddressTestnet, SolanaNetworks.TESTNET],
['devnet', getSolAddressDevnet, SolanaNetworks.DEVNET],
['local', getSolAddressLocal, SolanaNetworks.LOCAL]
] as const;

it.each(networkCases)(
'should generate valid %s address',
// eslint-disable-next-line local-rules/prefer-object-params
async (_, getAddress, networkType) => {
const result = await getAddress(mockIdentity);
expect(result).toBe(mockSolAddress);
expect(spyGetSchnorrPublicKey).toHaveBeenCalledWith({
identity: mockIdentity,
keyId: SOLANA_KEY_ID,
derivationPath: [SOLANA_DERIVATION_PATH_PREFIX, networkType]
});
}
);
});

describe('Load Solana Addresses into State', () => {
beforeEach(() => {
spyGetSchnorrPublicKey.mockResolvedValue(mockPublicKey);
});

const loadCases = [
['mainnet', loadSolAddressMainnet, solAddressMainnetStore],
['testnet', loadSolAddressTestnet, solAddressTestnetStore],
['devnet', loadSolAddressDevnet, solAddressDevnetStore],
['local', loadSolAddressLocal, solAddressLocalnetStore]
] as const;

// eslint-disable-next-line local-rules/prefer-object-params
it.each(loadCases)('should load %s address into store', async (_, loadAddress, store) => {
const result = await loadAddress();
expect(result).toEqual({ success: true });
expect(get(store)).toEqual({
data: mockSolAddress,
certified: true
});
});

it('should handle errors during address loading', async () => {
const error = new Error('Failed to load address');
spyGetSchnorrPublicKey.mockRejectedValue(error);

const result = await loadSolAddressMainnet();

expect(result).toEqual({ success: false });
expect(spyToastsError).toHaveBeenCalledWith({
msg: {
text: replacePlaceholders(en.init.error.loading_address, {
$symbol: SOLANA_TOKEN_ID.description ?? ''
})
},
err: error
});
});
});

describe('Load Address from IndexedDB', () => {
it('should successfully load address from IDB', async () => {
spyGetIdbAddress.mockResolvedValue({ address: mockSolAddress });

const result = await loadIdbSolAddressMainnet();

expect(result).toEqual({ success: true });
expect(get(solAddressMainnetStore)).toEqual({
data: mockSolAddress,
certified: false
});
expect(spyUpdateIdbAddressLastUsage).toHaveBeenCalledWith(mockIdentity.getPrincipal());
});

it('should handle missing IDB address', async () => {
spyGetIdbAddress.mockResolvedValue(undefined);

const result = await loadIdbSolAddressMainnet();

expect(result).toEqual({
success: false,
err: new LoadIdbAddressError(SOLANA_TOKEN_ID)
});
expect(spyUpdateIdbAddressLastUsage).not.toHaveBeenCalled();
});
});

describe('Validate and Certify Mainnet Address', () => {
it('should validate and certify a matching address', async () => {
spyGetSchnorrPublicKey.mockResolvedValue(mockPublicKey);

const addressStore = {
data: mockSolAddress,
certified: false
};

await validateSolAddressMainnet(addressStore);

expect(get(solAddressMainnetStore)).toEqual({
data: mockSolAddress,
certified: true
});
});
});
});

0 comments on commit 405d36c

Please sign in to comment.