UNPKG

@nimiq/network-client

Version:
235 lines (232 loc) 9.76 kB
import { EventClient } from '@nimiq/rpc-events'; // tslint:enable:interface-over-type-literal class NetworkClient { constructor(endpoint = NetworkClient.DEFAULT_ENDPOINT) { this._apiLoadingState = 'not-started'; this._consensusState = 'syncing'; this._peerCount = 0; this._headInfo = { height: 0, globalHashrate: 0 }; this._balances = new Map(); this._pendingTransactions = new Map(); this._expiredTransactions = []; this._minedTransactions = new Map(); this._relayedTransactions = new Map(); this._endpoint = endpoint + '/v2/'; } static get DEFAULT_ENDPOINT() { return window.location.origin.endsWith('nimiq.com') ? 'https://network.nimiq.com' : 'https://network.nimiq-testnet.com'; } static createInstance(endPoint = NetworkClient.DEFAULT_ENDPOINT) { if (NetworkClient._instance) throw new Error('NetworkClient already instantiated.'); const networkClient = new NetworkClient(endPoint); NetworkClient._instance = networkClient; return networkClient; } static hasInstance() { return !!NetworkClient._instance; } static get Instance() { return NetworkClient._instance || (NetworkClient._instance = new NetworkClient()); } static getAllowedOrigin(endpoint) { const url = new URL(endpoint); return url.origin; } static async _createIframe(src) { const $iframe = document.createElement('iframe'); const promise = new Promise((resolve) => $iframe.addEventListener('load', () => resolve($iframe))); $iframe.src = src; $iframe.name = 'NimiqNetwork'; $iframe.style.display = 'none'; document.body.appendChild($iframe); return promise; } async init() { this._initializationPromise = this._initializationPromise || (async () => { this.$iframe = await NetworkClient._createIframe(this._endpoint); const targetWindow = this.$iframe.contentWindow; this._eventClient = await EventClient.create(targetWindow, NetworkClient.getAllowedOrigin(this._endpoint)); this.on(NetworkClient.Events.API_READY, () => this._apiLoadingState = 'ready'); this.on(NetworkClient.Events.API_FAIL, () => this._apiLoadingState = 'failed'); this.on(NetworkClient.Events.CONSENSUS_SYNCING, () => this._consensusState = 'syncing'); this.on(NetworkClient.Events.CONSENSUS_ESTABLISHED, () => this._consensusState = 'established'); this.on(NetworkClient.Events.CONSENSUS_LOST, () => this._consensusState = 'lost'); this.on(NetworkClient.Events.PEERS_CHANGED, (peerCount) => this._peerCount = peerCount); this.on(NetworkClient.Events.BALANCES_CHANGED, (balances) => this._balances = balances); this.on(NetworkClient.Events.TRANSACTION_PENDING, (tx) => this._pendingTransactions.set(tx.hash, tx)); this.on(NetworkClient.Events.TRANSACTION_EXPIRED, (txHash) => { this._expiredTransactions.push([this.headInfo.height, txHash]); this._pendingTransactions.delete(txHash); this._relayedTransactions.delete(txHash); }); this.on(NetworkClient.Events.TRANSACTION_MINED, (tx) => { this._minedTransactions.set(tx.hash, tx); this._pendingTransactions.delete(tx.hash); this._relayedTransactions.delete(tx.hash); }); this.on(NetworkClient.Events.TRANSACTION_RELAYED, (tx) => { tx.blockHeight = this.headInfo.height; this._relayedTransactions.set(tx.hash, tx); }); this.on(NetworkClient.Events.HEAD_CHANGE, (headInfo) => { this._headInfo = headInfo; this._evictCachedTransactions(); }); })(); try { await this._initializationPromise; } catch (e) { delete this._initializationPromise; throw e; } } async on(event, callback) { this._eventClient.on(event, callback); } async off(event, callback) { this._eventClient.off(event, callback); } async connect() { return this._eventClient.call('connect'); } async disconnect(reason) { return this._eventClient.call('disconnect', reason); } async relayTransaction(txObj) { return this._eventClient.call('relayTransaction', txObj); } async getTransactionSize(txObj) { return this._eventClient.call('getTransactionSize', txObj); } async subscribe(addresses) { return this._eventClient.call('subscribe', addresses); } async getBalance(addresses) { return this._eventClient.call('getBalance', addresses); } async forgetBalances(addresses) { return this._eventClient.call('forgetBalances', addresses); } async getAccounts(addresses) { return this._eventClient.call('getAccounts', addresses); } async getAccountTypeString(address) { return this._eventClient.call('getAccountTypeString', address); } async requestTransactionHistory(addresses, // userfriendly addresses knownReceipts, // Map<txhash (base64), blockhash (base64)> fromHeight) { return this._eventClient.call('requestTransactionHistory', addresses, knownReceipts, fromHeight); } async requestTransactionReceipts(addresses, limit) { return this._eventClient.call('requestTransactionReceipts', addresses, limit); } async getGenesisVestingContracts(modern) { return this._eventClient.call('getGenesisVestingContracts', modern); } async removeTxFromMempool(txObj) { return this._eventClient.call('removeTxFromMempool', txObj); } async getPeerAddresses() { return this._eventClient.call('getPeerAddresses'); } // MODERN async sendTransaction(tx) { return this._eventClient.call('sendTransaction', tx); } async getTransactionsByAddress(address, sinceHeight, knownDetails, limit) { return this._eventClient.call('getTransactionsByAddress', address, sinceHeight, knownDetails, limit); } async addTransactionListener(listener, addresses) { const eventName = `transaction-listener-${Math.round(Math.random() * 1e8)}`; this._eventClient.on(eventName, listener); return this._eventClient.call('addTransactionListener', eventName, addresses); } async addConsensusChangedListener(listener) { const eventName = `consensus-listener-${Math.round(Math.random() * 1e8)}`; this._eventClient.on(eventName, listener); return this._eventClient.call('addConsensusChangedListener', eventName); } async resetConsensus() { return this._eventClient.call('resetConsensus'); } async removeListener(handle) { return this._eventClient.call('removeListener', handle); } // Getter get apiLoadingState() { return this._apiLoadingState; } get consensusState() { return this._consensusState; } get peerCount() { return this._peerCount; } get headInfo() { return this._headInfo; } get balances() { return this._balances; } get pendingTransactions() { return this._pendingTransactions.values(); } get minedTransactions() { return this._minedTransactions.values(); } get relayedTransactions() { return this._relayedTransactions.values(); } /** @returns base64 transaction hashes */ get expiredTransactions() { return this._expiredTransactions.map(([height, txHash]) => txHash); } // Private methods _evictCachedTransactions() { const CACHE_DURATION = 30; // purge expired transactions for (let i = 0; i < this._expiredTransactions.length; ++i) { const [expiredAt] = this._expiredTransactions[i]; if (expiredAt + CACHE_DURATION <= this.headInfo.height) { this._expiredTransactions.splice(i, 1); --i; } } // purge mined transactions for (const tx of this._minedTransactions.values()) { if (tx.blockHeight + CACHE_DURATION <= this.headInfo.height) { this._minedTransactions.delete(tx.hash); } } } } NetworkClient._instance = null; (function (NetworkClient) { let Events; (function (Events) { Events["API_READY"] = "nimiq-api-ready"; Events["API_FAIL"] = "nimiq-api-fail"; Events["CONSENSUS_SYNCING"] = "nimiq-consensus-syncing"; Events["CONSENSUS_ESTABLISHED"] = "nimiq-consensus-established"; Events["CONSENSUS_LOST"] = "nimiq-consensus-lost"; Events["PEERS_CHANGED"] = "nimiq-peer-count"; Events["BALANCES_CHANGED"] = "nimiq-balances"; Events["TRANSACTION_PENDING"] = "nimiq-transaction-pending"; Events["TRANSACTION_EXPIRED"] = "nimiq-transaction-expired"; Events["TRANSACTION_MINED"] = "nimiq-transaction-mined"; Events["TRANSACTION_RELAYED"] = "nimiq-transaction-relayed"; Events["HEAD_CHANGE"] = "nimiq-head-change"; Events["HEAD_HEIGHT"] = "head-height"; Events["CONSENSUS"] = "consensus"; Events["BALANCES"] = "balances"; Events["TRANSACTION"] = "transaction"; Events["PEER_COUNT"] = "peer-count"; Events["PEER_ADDRESSES_ADDED"] = "peer-addresses-added"; })(Events = NetworkClient.Events || (NetworkClient.Events = {})); })(NetworkClient || (NetworkClient = {})); export { NetworkClient };