Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into ADN-522-apply-dynamic…
Browse files Browse the repository at this point in the history
…-fees
  • Loading branch information
jinoosss committed Jan 13, 2025
2 parents b311b65 + 9e24e1c commit 4a2238a
Show file tree
Hide file tree
Showing 22 changed files with 316 additions and 34 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,26 @@ Also, follow us on our [Twitter](https://twitter.com/adenaapp) and [Medium](http
✅ Airgap Account Support <br>
✅ View & Transfer NFTs <br>
⬜ In-app Swap <br>
⬜ Multi-Chain
⬜ Multi-Chain

## Project Environment

| Tool | Version |
| ---------- | ------- |
| Node.js | 18.14.2 |
| TypeScript | 4.9.5 |
| Babel Core | 7.23.9 |
| Webpack | 5.90.3 |
| React | 18.2.0 |

## Building Locally

To set up a local environment, clone this repository and run the following commands:

```
nvm use
yarn install
yarn build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonError } from '@common/errors/common';
import { StorageMigrator, StorageModelLatest } from '@migrates/storage-migrator';
import { Storage } from '.';

// Define the valid types of storage keys
type StorageKeyTypes =
| 'NETWORKS'
| 'CURRENT_CHAIN_ID'
Expand All @@ -19,6 +20,7 @@ type StorageKeyTypes =
| 'ACCOUNT_GRC721_COLLECTIONS'
| 'ACCOUNT_GRC721_PINNED_PACKAGES';

// List of all available storage keys
const StorageKeys: StorageKeyTypes[] = [
'NETWORKS',
'CURRENT_CHAIN_ID',
Expand All @@ -37,11 +39,14 @@ const StorageKeys: StorageKeyTypes[] = [
'ACCOUNT_GRC721_PINNED_PACKAGES',
];

// Function to validate if a given key is a valid storage key
function isStorageKey(key: string): key is StorageKeyTypes {
return StorageKeys.some((storageKey) => storageKey === key);
}

// Class to handle Chrome's local storage with migration support
export class ChromeLocalStorage implements Storage {
// Define the main storage key for the application
private static StorageKey = 'ADENA_DATA';

private storage: chrome.storage.LocalStorageArea;
Expand Down Expand Up @@ -71,10 +76,12 @@ export class ChromeLocalStorage implements Storage {
};

public remove = async (key: string): Promise<void> => {
return this.set(key, '');
await this.set(key, '');
await this.storage.remove(key);
};

public clear = async (): Promise<void> => {
await this.storage.set({ [ChromeLocalStorage.StorageKey]: '{}' });
await this.storage.clear();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class ChromeSessionStorage implements Storage {
};

public remove = async (key: string): Promise<void> => {
await this.set(key, '');
await this.storage.remove(key);
};

Expand Down
3 changes: 2 additions & 1 deletion packages/adena-extension/src/common/utils/amount-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GNOT_TOKEN } from '@common/constants/token.constant';
import { Amount } from '@types';
import BigNumber from 'bignumber.js';
import { convertTextToAmount } from './string-utils';
Expand All @@ -19,7 +20,7 @@ export function makeGnotAmountByRaw(amountRaw: string): Amount | null {
return gnotAmount;
}

export function parseTokenAmount(tokenAmount: string, denomination = 'ugnot'): number {
export function parseTokenAmount(tokenAmount: string, denomination = GNOT_TOKEN.denom): number {
const pattern = new RegExp(`^(\\d+)${denomination}$`);
const match = tokenAmount.match(pattern);

Expand Down
13 changes: 13 additions & 0 deletions packages/adena-extension/src/common/utils/browser-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ export const createPopupWindow = async (popupPath: string, state: object = {}):
});
});
};

export const isExtensionPopup = (): boolean => {
const views = chrome.extension.getViews({ type: 'popup' });
return views.length > 0 && views[0] === window;
};

export const isSeparatePopupWindow = (): boolean => {
if (isExtensionPopup()) {
return false;
}

return window.opener || chrome.extension.getViews({ type: 'popup' }).length === 0;
};
5 changes: 4 additions & 1 deletion packages/adena-extension/src/common/utils/crypto-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { v4 as uuidv4 } from 'uuid';
import CryptoJS from 'crypto-js';
import { v4 as uuidv4 } from 'uuid';

// Static cipher key used for encrypting the cryptographic key
const ENCRYPT_CIPHER_KEY = 'r3v4';

export const encryptSha256Password = (password: string): string => {
return CryptoJS.SHA256(password).toString();
};

// Encrypts a password with a dynamically generated key and returns the encrypted key and password
export const encryptPassword = (
password: string,
): { encryptedKey: string; encryptedPassword: string } => {
Expand All @@ -20,6 +22,7 @@ export const encryptPassword = (
};
};

// Decrypts a password using the encrypted key and password
export const decryptPassword = (encryptedKey: string, encryptedPassword: string): string => {
const adenaKey = ENCRYPT_CIPHER_KEY;
const key = CryptoJS.AES.decrypt(encryptedKey, adenaKey).toString(CryptoJS.enc.Utf8);
Expand Down
4 changes: 4 additions & 0 deletions packages/adena-extension/src/hooks/use-clear.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BalanceState, CommonState, NetworkState, WalletState } from '@states';
import { useQueryClient } from '@tanstack/react-query';
import { useResetRecoilState, useSetRecoilState } from 'recoil';
import { useAdenaContext } from './use-context';
import useExtensionWindowManager from './use-extension-window-manager';

export type UseClearReturn = {
clear: () => Promise<boolean>;
Expand All @@ -26,6 +27,8 @@ export const useClear = (): UseClearReturn => {
const clearAccountTokenBalances = useResetRecoilState(BalanceState.accountTokenBalances);
const clearAddressBook = useResetRecoilState(WalletState.addressBook);

const { closeAllExtensionWindows } = useExtensionWindowManager();

const clear = async (): Promise<boolean> => {
setWalletState('CREATE');
clearTransactionHistory();
Expand All @@ -35,6 +38,7 @@ export const useClear = (): UseClearReturn => {
clearAccountTokenBalances();
clearCurrentNetwork();
clearAddressBook();
closeAllExtensionWindows();
await walletService.clear();
await accountService.clear();
await addressBookService.clear();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

const useExtensionWindowManager = () => {
const closeAllExtensionWindows = React.useCallback(async () => {
const windows = await chrome.windows.getAll();
for (const window of windows) {
if (window.type === 'popup' && window.id) {
await chrome.windows.remove(window.id);
}
}
}, []);

return { closeAllExtensionWindows };
};

export default useExtensionWindowManager;
3 changes: 2 additions & 1 deletion packages/adena-extension/src/hooks/use-faucet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GNOT_TOKEN } from '@common/constants/token.constant';
import { waitForRun } from '@common/utils/timeout-utils';
import { FaucetResponse } from '@repositories/common/response';
import { useMutation, useQuery } from '@tanstack/react-query';
Expand All @@ -11,7 +12,7 @@ export type UseFaucetReturn = {
faucet: () => Promise<{ success: boolean; message: string }>;
};

const FAUCET_AMOUNT = 10_000_000 + 'ugnot';
const FAUCET_AMOUNT = 10_000_000 + GNOT_TOKEN.denom;

const FAUCET_UNEXPECTED_ERROR_MESSAGES = 'Unexpected Errors.';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const useWalletExportScreen = (): UseWalletExportReturn => {
setExportType('PRIVATE_KEY');
break;
case 'SEED_PHRASE':
setExportAccountId(exportAccountId);
setExportType('SEED_PHRASE');
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('StorageMigrator', () => {
const migrated = await migrator.migrate(current);

expect(migrated).not.toBeNull();
expect(migrated?.version).toBe(7);
expect(migrated?.version).toBe(8);
expect(migrated?.data).not.toBeNull();
expect(migrated?.data.NETWORKS).toHaveLength(3);
expect(migrated?.data.CURRENT_CHAIN_ID).toBe('');
Expand All @@ -89,7 +89,7 @@ describe('StorageMigrator', () => {
const migrated = await migrator.migrate(current);

expect(migrated).not.toBeNull();
expect(migrated?.version).toBe(7);
expect(migrated?.version).toBe(8);
expect(migrated?.data).not.toBeNull();
expect(migrated?.data.SERIALIZED).not.toBe('');
expect(migrated?.data.ADDRESS_BOOK).toBe('');
Expand Down
32 changes: 27 additions & 5 deletions packages/adena-extension/src/migrates/storage-migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { StorageMigration006 } from './migrations/v006/storage-migration-v006';
import { StorageModelV006 } from './migrations/v006/storage-model-v006';
import { StorageMigration007 } from './migrations/v007/storage-migration-v007';
import { StorageModelV007 } from './migrations/v007/storage-model-v007';
import { StorageMigration008 } from './migrations/v008/storage-migration-v008';
import { StorageModelV008 } from './migrations/v008/storage-model-v008';
import { Migration, Migrator } from './migrator';

const LegacyStorageKeys = [
Expand All @@ -27,8 +29,10 @@ const LegacyStorageKeys = [
'ACCOUNT_TOKEN_METAINFOS',
];

export type StorageModelLatest = StorageModelV007;
// The latest storage model type
export type StorageModelLatest = StorageModelV008;

// Default data structure for version 1 storage model
const defaultData: StorageModelDataV001 = {
ACCOUNT_NAMES: {},
ADDRESS_BOOK: {},
Expand All @@ -42,24 +46,28 @@ const defaultData: StorageModelDataV001 = {
ACCOUNT_TOKEN_METAINFOS: {},
};

// Storage interface with set and get methods
interface Storage {
set(items: { [key: string]: any }): Promise<void>;
get(keys?: string | string[] | { [key: string]: any } | null): Promise<{ [key: string]: any }>;
}

// Handles storage migrations and serialization/deserialization of storage data
export class StorageMigrator implements Migrator {
private static StorageKey = 'ADENA_DATA';

constructor(
private migrations: Migration[],
private storage: Storage,
private password?: string,
private migrations: Migration[], // Array of migration strategies
private storage: Storage, // Storage interface for data persistence
private password?: string, // Password for encryption (optional)
) {}

// Sets the encryption password
setPassword(password: string): void {
this.password = password;
}

// Validates if the data can be saved
async saveable(): Promise<boolean | '' | undefined> {
const current = await this.getCurrent();
const latestVersion = Math.max(...this.migrations.map((m) => m.version));
Expand All @@ -72,13 +80,16 @@ export class StorageMigrator implements Migrator {
return this.password && this.password.length > 0;
}

// Serializes the storage model to a string
async serialize(data: StorageModel<unknown>): Promise<string> {
return JSON.stringify(data);
}

// Deserializes a string into the corresponding storage model
async deserialize(
data: string | undefined,
): Promise<
| StorageModelV008
| StorageModelV007
| StorageModelV006
| StorageModelV005
Expand All @@ -98,7 +109,9 @@ export class StorageMigrator implements Migrator {
return this.mappedJson(jsonData);
}

// Retrieves the current storage data, performing deserialization
async getCurrent(): Promise<
| StorageModelV008
| StorageModelV007
| StorageModelV006
| StorageModelV005
Expand All @@ -118,7 +131,8 @@ export class StorageMigrator implements Migrator {
};
}

async migrate(current: StorageModel): Promise<StorageModelV007 | null> {
// Migrates storage data to the latest version
async migrate(current: StorageModel): Promise<StorageModelV008 | null> {
let latest = current;
try {
const currentVersion = current.version || 1;
Expand All @@ -137,6 +151,7 @@ export class StorageMigrator implements Migrator {
return latest as StorageModelLatest;
}

// Saves the latest version of the storage data
async save(latest: StorageModel): Promise<void> {
if (!(await this.saveable())) {
throw new Error('Unable to save');
Expand All @@ -147,16 +162,19 @@ export class StorageMigrator implements Migrator {
});
}

// Creates a backup of the current storage data
private async backup(current: StorageModel): Promise<void> {
const backupStorageKey = `${StorageMigrator.StorageKey}_${Date.now()}`;
const savedData = await this.serialize(current);
await this.storage.set({ [backupStorageKey]: savedData });
}

// Maps JSON data to the corresponding storage model version
private async mappedJson(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
json: any,
): Promise<
| StorageModelV008
| StorageModelV007
| StorageModelV006
| StorageModelV005
Expand All @@ -165,6 +183,9 @@ export class StorageMigrator implements Migrator {
| StorageModelV002
| StorageModelV001
> {
if (json?.version === 8) {
return json as StorageModelV008;
}
if (json?.version === 7) {
return json as StorageModelV007;
}
Expand Down Expand Up @@ -218,6 +239,7 @@ export class StorageMigrator implements Migrator {
new StorageMigration005(),
new StorageMigration006(),
new StorageMigration007(),
new StorageMigration008(),
];
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import styled, { CSSProp, css } from 'styled-components';
import styled, { css, CSSProp } from 'styled-components';

import { Text, Button } from '@components/atoms';
import { isSeparatePopupWindow } from '@common/utils/browser-utils';
import { Button, Text } from '@components/atoms';
import { TitleWithDesc } from '@components/molecules';
import { RoutePath } from '@types';
import useAppNavigate from '@hooks/use-app-navigate';
import { useWalletContext } from '@hooks/use-context';
import mixins from '@styles/mixins';
import useAppNavigate from '@hooks/use-app-navigate';
import { RoutePath } from '@types';

const text = {
title: 'You’re All Set!',
Expand Down Expand Up @@ -41,11 +42,17 @@ export const LaunchAdena = (): JSX.Element => {
if (clicked) {
return;
}

setClicked(true);
if (params.type === 'GOOGLE' || params.type === 'LEDGER') {
window.close();
}

Promise.all([initWallet(), initNetworkMetainfos()]).then(() => {
if (isSeparatePopupWindow()) {
window.close();
}

navigate(RoutePath.Wallet);
setClicked(false);
});
Expand Down
Loading

0 comments on commit 4a2238a

Please sign in to comment.