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

V11.0.0 #22

Merged
merged 7 commits into from Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -15,7 +15,7 @@
"preview": "vite preview"
},
"dependencies": {
"@fireblocks/ncw-js-sdk": "10.0.7",
"@fireblocks/ncw-js-sdk": "11.0.0",
"base58-js": "^2.0.0",
"classnames": "^2.3.2",
"js-base64": "^3.7.5",
Expand Down
51 changes: 45 additions & 6 deletions src/AppStore.ts
@@ -1,16 +1,19 @@
import {
ConsoleLogger,
FireblocksNCW,
ConsoleLoggerFactory,
FireblocksNCWFactory,
IEventsHandler,
IFireblocksNCW,
IJoinWalletEvent,
IKeyBackupEvent,
IKeyDescriptor,
IKeyRecoveryEvent,
ILogger,
IMessagesHandler,
SigningInProgressError,
TEnv,
TEvent,
TMPCAlgorithm,
version as fireblocksNCWSdkVersion,
} from "@fireblocks/ncw-js-sdk";
import { create } from "zustand";
import { IAppState, IPassphraseInfo, TPassphrases, TAppMode, INewTransactionData } from "./IAppState";
Expand All @@ -21,6 +24,8 @@ import { PasswordEncryptedLocalStorage } from "./services/PasswordEncryptedLocal
import { IAuthManager } from "./auth/IAuthManager";
import { FirebaseAuthManager } from "./auth/FirebaseAuthManager";
import { decode } from "js-base64";
import { IndexedDBLoggerFactory } from "./logger/IndexedDBLogger.factory";
import { IndexedDBLogger } from "./logger/IndexedDBLogger";

export type TAsyncActionStatus = "not_started" | "started" | "success" | "failed";
export type TFireblocksNCWStatus = "sdk_not_ready" | "initializing_sdk" | "sdk_available" | "sdk_initialization_failed";
Expand All @@ -29,7 +34,8 @@ export type TRequestDecodedData = { email: string; requestId: string; platform:
export const useAppStore = create<IAppState>()((set, get) => {
let apiService: ApiService | null = null;
let txsUnsubscriber: (() => void) | null = null;
let fireblocksNCW: FireblocksNCW | null = null;
let fireblocksNCW: IFireblocksNCW | null = null;
let logger: IndexedDBLogger | null = null;
const authManager: IAuthManager = new FirebaseAuthManager();
authManager.onUserChanged((user) => {
set({ loggedUser: user });
Expand All @@ -46,7 +52,7 @@ export const useAppStore = create<IAppState>()((set, get) => {
};

return {
fireblocksNCWSdkVersion: FireblocksNCW.version,
fireblocksNCWSdkVersion,
automateInitialization: ENV_CONFIG.AUTOMATE_INITIALIZATION,
joinExistingWalletMode: false,
loggedUser: authManager.loggedUser,
Expand Down Expand Up @@ -359,14 +365,16 @@ export const useAppStore = create<IAppState>()((set, get) => {
}
return Promise.resolve(password || "");
});
logger = await IndexedDBLoggerFactory({ deviceId, logger: ConsoleLoggerFactory() });

fireblocksNCW = await FireblocksNCW.initialize({
fireblocksNCW = await FireblocksNCWFactory({
env: ENV_CONFIG.NCW_SDK_ENV as TEnv,
logLevel: "INFO",
deviceId,
messagesHandler,
eventsHandler,
secureStorageProvider,
logger: new ConsoleLogger(),
logger,
});

txsUnsubscriber = apiService.listenToTxs(deviceId, (tx: ITransactionData) => {
Expand Down Expand Up @@ -683,5 +691,36 @@ export const useAppStore = create<IAppState>()((set, get) => {
),
}));
},
collectLogs: async () => {
if (!logger) {
return;
}
const limitInput = await prompt("Enter number of logs to collect (empty for all)");
const limit = limitInput ? parseInt(limitInput) : null;
const logs = await logger.collect(limit);
const logsString = logs.map((log) => JSON.stringify(log)).join("\n");

const downloadLink = document.createElement("a");
downloadLink.href = URL.createObjectURL(new Blob([logsString], { type: "text/plain" }));
downloadLink.download = `${Date.now()}_ncw-sdk-logs.log`;
downloadLink.click();
logger.log("INFO", `Collected ${logs.length} logs`);
},
clearLogs: async () => {
if (!logger) {
return;
}
const limitInput = await prompt("Enter number of logs to clear (empty for all)");
const limit = limitInput ? parseInt(limitInput) : null;
const clearedLogsNum = await logger.clear(limit);
logger.log("INFO", `Cleared logs: ${clearedLogsNum}`);
},
countLogs: async () => {
if (!logger) {
return;
}
const numberOfLogs = await logger.count();
logger.log("INFO", `Number of logs: ${numberOfLogs}`);
},
};
});
3 changes: 3 additions & 0 deletions src/IAppState.ts
Expand Up @@ -117,4 +117,7 @@ export interface IAppState {
refreshSupportedAssets: (accountId: number) => Promise<void>;
refreshAddress: (accountId: number, assetId: string) => Promise<void>;
addAsset: (accountId: number, assetId: string) => Promise<void>;
countLogs: () => Promise<void>;
clearLogs: () => Promise<void>;
collectLogs: () => Promise<void>;
}
2 changes: 2 additions & 0 deletions src/components/FireblocksNCWExampleActions.tsx
Expand Up @@ -8,6 +8,7 @@ import { Transactions } from "./Transactions";
import { Web3 } from "./Web3";
import { useAppStore } from "../AppStore";
import { JoinExistingWallet } from "./JoinExistingWallet";
import { Logs } from "./Logs";

export const FireblocksNCWExampleActions: React.FC = () => {
const { keysStatus, joinExistingWalletMode } = useAppStore();
Expand All @@ -26,6 +27,7 @@ export const FireblocksNCWExampleActions: React.FC = () => {
<Transactions />
<Web3 />
<Takeover />
<Logs />
</>
)}
</>
Expand Down
21 changes: 21 additions & 0 deletions src/components/Logs.tsx
@@ -0,0 +1,21 @@
import React from "react";

import { useAppStore } from "../AppStore";
import { Card, ICardAction } from "./ui/Card";

export const Logs: React.FC = () => {
const { collectLogs, clearLogs, countLogs } = useAppStore();
const collectLogsAction: ICardAction = {
label: "Collect Logs",
action: collectLogs,
};
const clearLogsAction: ICardAction = {
label: "Clear Logs",
action: clearLogs,
};
const countLogsAction: ICardAction = {
label: "Count Logs",
action: countLogs,
};
return <Card title="Logs" actions={[collectLogsAction, clearLogsAction, countLogsAction]}></Card>;
};
9 changes: 9 additions & 0 deletions src/logger/IndexedDBLogger.factory.ts
@@ -0,0 +1,9 @@
import { IIndexedDBLoggerOptions, IndexedDBLogger } from "./IndexedDBLogger";

export function IndexedDBLoggerFactory(options: IIndexedDBLoggerOptions): Promise<IndexedDBLogger> {
if (!IndexedDBLogger.isIndexedDBAvailable()) {
throw new Error("IndexedDB is not available");
}

return IndexedDBLogger.initialize(options);
}
190 changes: 190 additions & 0 deletions src/logger/IndexedDBLogger.ts
@@ -0,0 +1,190 @@
import { ILogger, NullLoggerFactory, TLogLevel } from "@fireblocks/ncw-js-sdk";

const DB_NAME = "fireblocks-ncw-sdk-logs";
const TABLE_NAME = "logs";
const DEVICE_ID_INDEX = "deviceIdIndex";

export interface LogEntry {
timestamp: string;
deviceId: string;
level: TLogLevel;
message: string;
data?: any;
}
export interface IIndexedDBLoggerOptions {
deviceId: string;
logger?: ILogger;
dbName?: string;
tableName?: string;
}

export class IndexedDBLogger implements ILogger {
private _dbInstance: IDBDatabase | null = null;
private _dbName: string;
private _tableName: string;
private _deviceId: string;
private _logger: ILogger;

private constructor(options: IIndexedDBLoggerOptions) {
this._deviceId = options.deviceId;
this._logger = options.logger ?? NullLoggerFactory();
this._dbName = options.dbName ?? DB_NAME;
this._tableName = options.tableName ?? TABLE_NAME;
}

public static async initialize(options: IIndexedDBLoggerOptions): Promise<IndexedDBLogger> {
const logger = new IndexedDBLogger(options);
await logger._initializeDB();
return logger;
}

public static isIndexedDBAvailable(): boolean {
try {
return Boolean(globalThis.indexedDB) === true;
} catch (e) {
return false;
}
}

public log(level: TLogLevel, message: string, data?: any): void {
this._logger.log(level, message, data);

this._saveLog({
level,
message,
data,
deviceId: this._deviceId,
timestamp: new Date().toISOString(),
});
}

public collect(limit: number | null): Promise<LogEntry[]> {
return new Promise((resolve, reject) => {
if (!this._dbInstance) {
reject("IndexedDB is not initialized");
return;
}

const transaction = this._dbInstance.transaction(this._tableName, "readonly");
const store = transaction.objectStore(this._tableName);

const index = store.index(DEVICE_ID_INDEX);
const range = IDBKeyRange.only(this._deviceId);
const cursorRequest = index.openCursor(range, "prev");

const result: LogEntry[] = [];

cursorRequest.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (!cursor || result.length === limit) {
resolve(result);
} else {
result.push(cursor.value);
cursor.continue();
}
};

cursorRequest.onerror = () => {
reject("Error retrieving logs from IndexedDB");
};
});
}

public clear(limit: number | null): Promise<number> {
return new Promise((resolve, reject) => {
if (!this._dbInstance) {
reject("IndexedDB is not initialized");
return;
}

const transaction = this._dbInstance.transaction(this._tableName, "readwrite");
const store = transaction.objectStore(this._tableName);

const index = store.index(DEVICE_ID_INDEX);
const range = IDBKeyRange.only(this._deviceId);
const cursorRequest = index.openCursor(range);

let deleted = 0;
cursorRequest.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (!cursor || deleted === limit) {
resolve(deleted);
} else {
store.delete(cursor.primaryKey);
deleted++;
cursor.continue();
}
};

cursorRequest.onerror = () => {
reject("Error retrieving logs from IndexedDB");
};
});
}

public count(): Promise<number> {
return new Promise((resolve, reject) => {
if (!this._dbInstance) {
reject("IndexedDB is not initialized");
return;
}

const transaction = this._dbInstance.transaction(this._tableName, "readonly");
const store = transaction.objectStore(this._tableName);

const index = store.index(DEVICE_ID_INDEX);
const range = IDBKeyRange.only(this._deviceId);
const countRequest = index.count(range);

countRequest.onsuccess = (event) => {
const result = (event.target as IDBRequest).result;
resolve(result);
};

countRequest.onerror = () => {
reject("Error retrieving logs from IndexedDB");
};
});
}

private _saveLog(logEntry: LogEntry): void {
if (!this._dbInstance) {
this._logger.log("ERROR", "IndexedDB is not initialized");
return;
}
const transaction = this._dbInstance.transaction(this._tableName, "readwrite");
const store = transaction.objectStore(this._tableName);

const addRequest = store.add(logEntry);

addRequest.onsuccess = () => {
this._logger.log("VERBOSE", "Message saved to IndexedDB");
};

addRequest.onerror = () => {
this._logger.log("ERROR", "Error saving message to IndexedDB");
};
}

private _initializeDB(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.open(this._dbName, 1);

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const objectStore = db.createObjectStore(this._tableName, { keyPath: "id", autoIncrement: true });

objectStore.createIndex(DEVICE_ID_INDEX, "deviceId");
};

request.onsuccess = (event) => {
this._dbInstance = (event.target as IDBOpenDBRequest).result;
resolve();
};

request.onerror = () => {
reject("Error opening IndexedDB");
};
});
}
}