diff --git a/__tests__/integration/hathorwallet_facade.test.ts b/__tests__/integration/hathorwallet_facade.test.ts index 189861430..0cf0b2ef2 100644 --- a/__tests__/integration/hathorwallet_facade.test.ts +++ b/__tests__/integration/hathorwallet_facade.test.ts @@ -39,6 +39,7 @@ import { TransactionTemplateBuilder } from '../../src/template/transaction'; import FeeHeader from '../../src/headers/fee'; import Header from '../../src/headers/base'; import CreateTokenTransaction from '../../src/models/create_token_transaction'; +import WalletConnection from '../../src/new/connection'; const fakeTokenUid = '008a19f84f2ae284f19bf3d03386c878ddd15b8b0b604a3a3539aa9d714686e1'; const sampleNftData = @@ -362,7 +363,12 @@ describe('start', () => { () => new HathorWallet({ seed: walletData.words, - connection: { state: ConnectionState.CONNECTED }, + connection: { + state: ConnectionState.CONNECTED, + getState(): ConnectionState { + return ConnectionState.CONNECTED; + }, + } as Partial, password: DEFAULT_PASSWORD, pinCode: DEFAULT_PIN_CODE, }) diff --git a/__tests__/new/hathorwallet.test.ts b/__tests__/new/hathorwallet.test.ts index 2708a8b9c..b939a9aa8 100644 --- a/__tests__/new/hathorwallet.test.ts +++ b/__tests__/new/hathorwallet.test.ts @@ -20,7 +20,8 @@ import { } from '../../src/constants'; import { MemoryStore, Storage } from '../../src/storage'; import Queue from '../../src/models/queue'; -import { WalletType } from '../../src/types'; +import { IHistoryTx, WalletType } from '../../src/types'; +import { WalletWebSocketData } from '../../src/new/types'; import txApi from '../../src/api/txApi'; import * as addressUtils from '../../src/utils/address'; import walletUtils from '../../src/utils/wallet'; @@ -370,13 +371,17 @@ test('processTxQueue', async () => { }; // wsTxQueue is not part of the prototype so it won't be faked on FakeHathorWallet - hWallet.wsTxQueue = new Queue(); - hWallet.wsTxQueue.enqueue(1); - hWallet.wsTxQueue.enqueue(2); - hWallet.wsTxQueue.enqueue(3); + hWallet.wsTxQueue = new Queue(); + hWallet.wsTxQueue.enqueue({ type: 'fakeType' }); + hWallet.wsTxQueue.enqueue({ type: 'fakeType' }); + hWallet.wsTxQueue.enqueue({ type: 'fakeType' }); await hWallet.processTxQueue(); - expect(processedTxs).toStrictEqual([1, 2, 3]); + expect(processedTxs).toStrictEqual([ + { type: 'fakeType' }, + { type: 'fakeType' }, + { type: 'fakeType' }, + ]); }); test('handleWebsocketMsg', async () => { @@ -389,8 +394,11 @@ test('handleWebsocketMsg', async () => { }); // wsTxQueue is not part of the prototype so it won't be faked on FakeHathorWallet - hWallet.wsTxQueue = new Queue(); - hWallet.wsTxQueue.enqueue({ type: 'wallet:address_history', history: [1] }); + hWallet.wsTxQueue = new Queue(); + hWallet.wsTxQueue.enqueue({ + type: 'wallet:address_history', + history: [1] as unknown as IHistoryTx, + }); hWallet.newTxPromise = Promise.resolve(); hWallet.state = HathorWallet.PROCESSING; @@ -559,6 +567,7 @@ test('getAddressPrivKey', async () => { getCurrentServer: jest.fn().mockReturnValue('https://fullnode'), on: jest.fn(), start: jest.fn(), + getCurrentNetwork: jest.fn().mockReturnValue('testnet'), }; jest.spyOn(versionApi, 'getVersion').mockImplementation(resolve => { @@ -596,6 +605,7 @@ test('signMessageWithAddress', async () => { getCurrentServer: jest.fn().mockReturnValue('https://fullnode'), on: jest.fn(), start: jest.fn(), + getCurrentNetwork: jest.fn().mockReturnValue('testnet'), }; jest.spyOn(versionApi, 'getVersion').mockImplementation(resolve => { @@ -725,6 +735,7 @@ test('start', async () => { getCurrentServer: jest.fn().mockReturnValue('https://fullnode'), on: jest.fn(), start: jest.fn(), + getCurrentNetwork: jest.fn().mockReturnValue('testnet'), }; jest.spyOn(versionApi, 'getVersion').mockImplementation(resolve => { diff --git a/jest-integration.config.js b/jest-integration.config.js index 1b1434b83..da74cd577 100644 --- a/jest-integration.config.js +++ b/jest-integration.config.js @@ -25,10 +25,10 @@ module.exports = { }, // We need a high coverage for the HathorWallet class './src/new/wallet.ts': { - statements: 92, - branches: 85, + statements: 90, + branches: 83, functions: 90, - lines: 92, + lines: 90, }, }, }; diff --git a/src/connection.ts b/src/connection.ts index de8c67761..60bb65254 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -112,6 +112,13 @@ abstract class Connection extends EventEmitter { this.emit('state', state); } + /** + * Get current connection state + */ + getState(): ConnectionState { + return this.state; + } + /** * Connect to the server and start emitting events. * */ diff --git a/src/new/sendTransaction.ts b/src/new/sendTransaction.ts index f24779c18..fd3f004e2 100644 --- a/src/new/sendTransaction.ts +++ b/src/new/sendTransaction.ts @@ -54,7 +54,7 @@ export function isDataOutput(output: ISendOutput): output is ISendDataOutput { } export interface ISendTokenOutput { - type: OutputType.P2PKH | OutputType.P2SH; + type: OutputType.P2PKH | OutputType.P2SH; // XXX: This type is ignored in the only place it is used address: string; value: OutputValueType; token: string; @@ -506,7 +506,7 @@ export default class SendTransaction extends EventEmitter { // This just returns if the transaction is not a CREATE_TOKEN_TX await addCreatedTokenFromTx(transaction as CreateTokenTransaction, storage); // Add new transaction to the wallet's storage. - wallet.enqueueOnNewTx({ history: historyTx }); + wallet.enqueueOnNewTx({ type: '', history: historyTx }); // FIXME: Add a type here })(this.wallet, this.storage, this.transaction); } this.emit('send-tx-success', this.transaction); diff --git a/src/new/wallet.ts b/src/new/wallet.ts index f86e6c49b..b6706201b 100644 --- a/src/new/wallet.ts +++ b/src/new/wallet.ts @@ -59,6 +59,7 @@ import { ApiVersion, AddressScanPolicyData, AuthorityType, + EcdsaTxSign, FullNodeVersionData, getDefaultLogger, HistorySyncMode, @@ -97,6 +98,7 @@ import { IHistoryTxSchema } from '../schemas'; import GLL from '../sync/gll'; import { TransactionTemplate, WalletTxTemplateInterpreter } from '../template/transaction'; import Address from '../models/address'; +import { ConnectionState, Utxo } from '../wallet/types'; import Transaction from '../models/transaction'; import { CreateNFTOptions, @@ -121,9 +123,11 @@ import { UtxoDetails, UtxoOptions, SendTransactionFullnodeOptions, + WalletWebSocketData, + WalletStartOptions, + WalletStopOptions, BuildTxTemplateOptions, } from './types'; -import { Utxo } from '../wallet/types'; import { FullNodeTxApiResponse, GraphvizNeighboursDotResponse, @@ -134,16 +138,7 @@ import { const ERROR_MESSAGE_PIN_REQUIRED = 'Pin is required.'; -/** - * TODO: This should be removed when this file is migrated to typescript - * we need this here because the typescript enum from the Connection file is - * not being correctly transpiled here, returning `undefined` for ConnectionState.CLOSED. - */ -const ConnectionState = { - CLOSED: 0, - CONNECTING: 1, - CONNECTED: 2, -}; +const ERROR_MESSAGE_PASSWORD_REQUIRED = 'Password is required.'; /** * This is a Wallet that is supposed to be simple to be used by a third-party app. @@ -210,7 +205,7 @@ class HathorWallet extends EventEmitter { multisig?: { pubkeys: string[]; numSignatures: number }; // Transaction queue - wsTxQueue: Queue; + wsTxQueue: Queue; newTxPromise: Promise; @@ -331,7 +326,7 @@ class HathorWallet extends EventEmitter { throw Error("You can't use xpriv with passphrase."); } - if (connection.state !== ConnectionState.CLOSED) { + if (connection.getState() !== ConnectionState.CLOSED) { throw Error("You can't share connections."); } @@ -407,7 +402,7 @@ class HathorWallet extends EventEmitter { }; } - this.wsTxQueue = new Queue(); + this.wsTxQueue = new Queue(); this.newTxPromise = Promise.resolve(); this.scanPolicy = scanPolicy; @@ -612,9 +607,9 @@ class HathorWallet extends EventEmitter { * Called when the connection to the websocket changes. * It is also called if the network is down. * - * @param {Number} newState Enum of new state after change + * @param newState The new connection state (0: CLOSED, 1: CONNECTING, 2: CONNECTED) */ - async onConnectionChangedState(newState: any): Promise { + async onConnectionChangedState(newState: ConnectionState): Promise { if (newState === ConnectionState.CONNECTED) { this.setState(HathorWallet.SYNCING); @@ -624,7 +619,7 @@ class HathorWallet extends EventEmitter { // before loading the full data again if (this.firstConnection) { this.firstConnection = false; - const addressesToLoad: any = await scanPolicyStartAddresses(this.storage); + const addressesToLoad = await scanPolicyStartAddresses(this.storage); await this.syncHistory(addressesToLoad.nextIndex, addressesToLoad.count); } else { if (this.beforeReloadCallback) { @@ -633,7 +628,7 @@ class HathorWallet extends EventEmitter { await this.reloadStorage(); } this.setState(HathorWallet.PROCESSING); - } catch (error: any) { + } catch (error) { this.setState(HathorWallet.ERROR); this.logger.error('Error loading wallet', { error }); } @@ -846,8 +841,10 @@ class HathorWallet extends EventEmitter { /** * Called when a new message arrives from websocket. + * + * @param wsData WebSocket message data */ - handleWebsocketMsg(wsData: any): any { + handleWebsocketMsg(wsData: WalletWebSocketData): void { if (wsData.type === 'wallet:address_history') { if (this.state !== HathorWallet.READY) { // Cannot process new transactions from ws when the wallet is not ready. @@ -1382,12 +1379,9 @@ class HathorWallet extends EventEmitter { /** * Process the transactions on the websocket transaction queue as if they just arrived. - * - * @memberof HathorWallet - * @inner */ - async processTxQueue(): Promise { - let wsData: any = this.wsTxQueue.dequeue(); + async processTxQueue(): Promise { + let wsData = this.wsTxQueue.dequeue(); while (wsData !== undefined) { // save new txdata @@ -1421,10 +1415,10 @@ class HathorWallet extends EventEmitter { /** * Call the method to process data and resume with the correct state after processing. * - * @returns {Promise} A promise that resolves when the wallet is done processing the tx queue. + * @returns A promise that resolves when the wallet is done processing the tx queue. */ - async onEnterStateProcessing() { - // Started processing state now, so we prepare the local data to support using this facade interchangable with wallet service facade in both wallets + async onEnterStateProcessing(): Promise { + // Started processing state now, so we prepare the local data to support using this facade interchangeable with wallet service facade in both wallets try { await this.processTxQueue(); this.setState(HathorWallet.READY); @@ -1447,16 +1441,19 @@ class HathorWallet extends EventEmitter { /** * Enqueue the call for onNewTx with the given data. - * @param {{ history: import('../types').IHistoryTx }} wsData + * + * @param wsData WebSocket message data containing transaction history */ - enqueueOnNewTx(wsData) { + enqueueOnNewTx(wsData: WalletWebSocketData): void { this.newTxPromise = this.newTxPromise.then(() => this.onNewTx(wsData)); } /** - * @param {{ history: import('../types').IHistoryTx }} wsData + * Process a new transaction received from websocket. + * + * @param wsData WebSocket message data containing transaction history */ - async onNewTx(wsData) { + async onNewTx(wsData: WalletWebSocketData): Promise { const parseResult = IHistoryTxSchema.safeParse(wsData.history); if (!parseResult.success) { this.logger.error(parseResult.error); @@ -1609,29 +1606,25 @@ class HathorWallet extends EventEmitter { /** * Connect to the server and start emitting events. * - * @param {Object} optionsParams Options parameters - * { - * 'pinCode': pin to decrypt xpriv information. Required if not set in object. - * 'password': password to decrypt xpriv information. Required if not set in object. - * } + * @param optionsParams Options parameters for starting the wallet */ - async start(optionsParams: any = {}): Promise { - const options: any = { pinCode: null, password: null, ...optionsParams }; - const pinCode: any = options.pinCode || this.pinCode; - const password: any = options.password || this.password; + async start(optionsParams: WalletStartOptions = {}): Promise { + const options = { pinCode: null, password: null, ...optionsParams }; + const pinCode = options.pinCode || this.pinCode; + const password = options.password || this.password; if (!this.xpub && !pinCode) { throw new Error(ERROR_MESSAGE_PIN_REQUIRED); } if (this.seed && !password) { - throw new Error('Password is required.'); + throw new Error(ERROR_MESSAGE_PASSWORD_REQUIRED); } // Check database consistency await this.storage.store.validate(); await this.storage.setScanningPolicyData(this.scanPolicy || null); - this.storage.config.setNetwork(this.conn.network); + this.storage.config.setNetwork(this.conn.getCurrentNetwork()); this.storage.config.setServerUrl(this.conn.getCurrentServer()); this.conn.on('state', this.onConnectionChangedState); this.conn.on('wallet-update', this.handleWebsocketMsg); @@ -1648,14 +1641,23 @@ class HathorWallet extends EventEmitter { let accessData = await this.storage.getAccessData(); if (!accessData) { if (this.seed) { + if (!pinCode) { + throw new Error(ERROR_MESSAGE_PIN_REQUIRED); + } + if (!password) { + throw new Error(ERROR_MESSAGE_PASSWORD_REQUIRED); + } accessData = walletUtils.generateAccessDataFromSeed(this.seed, { multisig: this.multisig, passphrase: this.passphrase, pin: pinCode, password, - networkName: this.conn.network, + networkName: this.conn.getCurrentNetwork(), }); } else if (this.xpriv) { + if (!pinCode) { + throw new Error(ERROR_MESSAGE_PIN_REQUIRED); + } accessData = walletUtils.generateAccessDataFromXpriv(this.xpriv, { multisig: this.multisig, pin: pinCode, @@ -1678,21 +1680,29 @@ class HathorWallet extends EventEmitter { const info = await new Promise((resolve, reject) => { versionApi.getVersion(resolve).catch(error => reject(error)); }); - if (info.network.indexOf(this.conn.network) >= 0) { + if (info.network.indexOf(this.conn.getCurrentNetwork()) >= 0) { this.storage.setApiVersion(info); await this.storage.saveNativeToken(); this.conn.start(); } else { this.setState(HathorWallet.CLOSED); - throw new Error(`Wrong network. server=${info.network} expected=${this.conn.network}`); + throw new Error( + `Wrong network. server=${info.network} expected=${this.conn.getCurrentNetwork()}` + ); } return info; } /** * Close the connections and stop emitting events. + * + * @param options Options for stopping the wallet */ - async stop({ cleanStorage = true, cleanAddresses = false, cleanTokens = false } = {}) { + async stop({ + cleanStorage = true, + cleanAddresses = false, + cleanTokens = false, + }: WalletStopOptions = {}): Promise { this.setState(HathorWallet.CLOSED); this.removeAllListeners(); @@ -3281,32 +3291,39 @@ class HathorWallet extends EventEmitter { /** * Set the external tx signing method. - * @param {EcdsaTxSign|null} method + * + * @param method The external transaction signing method, or null to clear */ - setExternalTxSigningMethod(method: any): any { + setExternalTxSigningMethod(method: EcdsaTxSign | null): void { this.isSignedExternally = !!method; this.storage.setTxSignatureMethod(method); } /** * Set the history sync mode. - * @param {HistorySyncMode} mode + * + * @param mode The history sync mode to use */ - setHistorySyncMode(mode: any): any { + setHistorySyncMode(mode: HistorySyncMode): void { this.historySyncMode = mode; } /** - * @param {number} startIndex - * @param {number} count - * @param {boolean} [shouldProcessHistory=false] - * @returns {Promise} + * Sync wallet history starting from a specific address index. + * + * @param startIndex The index of the first address to sync + * @param count The number of addresses to sync + * @param shouldProcessHistory If we should process the transaction history found */ - async syncHistory(startIndex: any, count: any, shouldProcessHistory: any = false): Promise { + async syncHistory( + startIndex: number, + count: number, + shouldProcessHistory: boolean = false + ): Promise { if (!(await getSupportedSyncMode(this.storage)).includes(this.historySyncMode)) { throw new Error('Trying to use an unsupported sync method for this wallet.'); } - let syncMode: any = this.historySyncMode; + let syncMode = this.historySyncMode; if ( [HistorySyncMode.MANUAL_STREAM_WS, HistorySyncMode.XPUB_STREAM_WS].includes( this.historySyncMode @@ -3321,7 +3338,7 @@ class HathorWallet extends EventEmitter { this.logger.debug('Falling back to http polling API'); syncMode = HistorySyncMode.POLLING_HTTP_API; } - const syncMethod: any = getHistorySyncMethod(syncMode); + const syncMethod = getHistorySyncMethod(syncMode); // This will add the task to the GLL queue and return a promise that // resolves when the task finishes executing await GLL.add(async () => { @@ -3330,23 +3347,23 @@ class HathorWallet extends EventEmitter { } /** - * Reload all addresses and transactions from the full node + * Reload all addresses and transactions from the full node. */ - async reloadStorage(): Promise { + async reloadStorage(): Promise { await this.conn.onReload(); // unsub all addresses for await (const address of this.storage.getAllAddresses()) { this.conn.unsubscribeAddress(address.base58); } - const accessData: any = await this.storage.getAccessData(); + const accessData = await this.storage.getAccessData(); if (accessData != null) { // Clean entire storage await this.storage.cleanStorage(true, true); // Reset access data await this.storage.saveAccessData(accessData); } - const addressesToLoad: any = await scanPolicyStartAddresses(this.storage); + const addressesToLoad = await scanPolicyStartAddresses(this.storage); await this.syncHistory(addressesToLoad.nextIndex, addressesToLoad.count); } diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 09a5c048f..f5cfc0f64 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -161,9 +161,9 @@ export class Storage implements IStorage { /** * Set the tx signing function - * @param {EcdsaTxSign} txSign The signing function + * @param txSign The signing function, or a null value to clear it */ - setTxSignatureMethod(txSign: EcdsaTxSign): void { + setTxSignatureMethod(txSign: EcdsaTxSign | null): void { this.txSignFunc = txSign; } diff --git a/src/types.ts b/src/types.ts index 9a8ee0b36..411168fe4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -587,7 +587,7 @@ export interface IStorage { setLogger(logger: ILogger): void; hasTxSignatureMethod(): boolean; - setTxSignatureMethod(txSign: EcdsaTxSign): void; + setTxSignatureMethod(txSign: EcdsaTxSign | null): void; getTxSignatures(tx: Transaction, pinCode: string): Promise; // Address methods