UNPKG

alchemy-sdk

Version:
1,147 lines (1,143 loc) 46.7 kB
import { _ as __awaiter, f as fromHex, t as toHex, n as noop, C as CustomNetworks, i as isAlchemyEvent, v as verifyAlchemyEventName, c as EthersEvent, e as getAlchemyEventTag, D as DEFAULT_ALCHEMY_API_KEY, A as ALCHEMY_EVENT_TYPES, h as ALCHEMY_PENDING_TRANSACTIONS_EVENT_TYPE, j as AlchemySubscription, k as ALCHEMY_MINED_TRANSACTIONS_EVENT_TYPE, V as VERSION, E as EthersNetwork } from './index-1789de96.js'; import SturdyWebSocket from 'sturdy-websocket'; import { BigNumber } from '@ethersproject/bignumber'; import { getNetwork } from '@ethersproject/networks'; import { WebSocketProvider } from '@ethersproject/providers'; import { AlchemyProvider } from './alchemy-provider-8b0c3e20.js'; import './api/utils'; import 'axios'; import '@ethersproject/abstract-provider'; import '@ethersproject/wallet'; import '@ethersproject/contracts'; import '@ethersproject/web'; /** * The maximum number of blocks to backfill. If more than this many blocks have * been missed, then we'll sadly miss data, but we want to make sure we don't * end up requesting thousands of blocks if somebody left their laptop closed for a week. */ const MAX_BACKFILL_BLOCKS = 120; /** * The WebsocketBackfiller fetches events that were sent since a provided block * number. This is used in the {@link AlchemyWebSocketProvider} to backfill * events that were transmitted while the websocket connection was down. * * The backfiller backfills two main eth_subscribe events: `logs` and `newHeads`. * * @internal */ class WebsocketBackfiller { constructor(provider) { this.provider = provider; // TODO: Use HTTP provider to do backfill. this.maxBackfillBlocks = MAX_BACKFILL_BLOCKS; } /** * Runs backfill for `newHeads` events. * * @param isCancelled Whether the backfill request is cancelled. * @param previousHeads Previous head requests that were sent. * @param fromBlockNumber The block number to start backfilling from. * @returns A list of `newHeads` events that were sent since the last backfill. */ getNewHeadsBackfill(isCancelled, previousHeads, fromBlockNumber) { return __awaiter(this, void 0, void 0, function* () { throwIfCancelled(isCancelled); const toBlockNumber = yield this.getBlockNumber(); throwIfCancelled(isCancelled); // If there are no previous heads to fetch, return new heads since // `fromBlockNumber`, or up to maxBackfillBlocks from the current head. if (previousHeads.length === 0) { return this.getHeadEventsInRange(Math.max(fromBlockNumber, toBlockNumber - this.maxBackfillBlocks) + 1, toBlockNumber + 1); } // If the last emitted event is too far back in the past, there's no need // to backfill for reorgs. Just fetch the last `maxBackfillBlocks` worth of // new heads. const lastSeenBlockNumber = fromHex(previousHeads[previousHeads.length - 1].number); const minBlockNumber = toBlockNumber - this.maxBackfillBlocks + 1; if (lastSeenBlockNumber <= minBlockNumber) { return this.getHeadEventsInRange(minBlockNumber, toBlockNumber + 1); } // To capture all `newHeads` events, return all head events from the last // seen block number to current + any of the previous heads that were re-orged. const reorgHeads = yield this.getReorgHeads(isCancelled, previousHeads); throwIfCancelled(isCancelled); const intermediateHeads = yield this.getHeadEventsInRange(lastSeenBlockNumber + 1, toBlockNumber + 1); throwIfCancelled(isCancelled); return [...reorgHeads, ...intermediateHeads]; }); } /** * Runs backfill for `logs` events. * * @param isCancelled Whether the backfill request is cancelled. * @param filter The filter object that accompanies a logs subscription. * @param previousLogs Previous log requests that were sent. * @param fromBlockNumber The block number to start backfilling from. */ getLogsBackfill(isCancelled, filter, previousLogs, fromBlockNumber) { return __awaiter(this, void 0, void 0, function* () { throwIfCancelled(isCancelled); const toBlockNumber = yield this.getBlockNumber(); throwIfCancelled(isCancelled); // If there are no previous logs to fetch, return new logs since // `fromBlockNumber`, or up to `maxBackfillBlocks` from the current head. if (previousLogs.length === 0) { return this.getLogsInRange(filter, Math.max(fromBlockNumber, toBlockNumber - this.maxBackfillBlocks) + 1, toBlockNumber + 1); } // If the last emitted log is too far back in the past, there's no need // to backfill for removed logs. Just fetch the last `maxBackfillBlocks` // worth of logs. const lastSeenBlockNumber = fromHex(previousLogs[previousLogs.length - 1].blockNumber); const minBlockNumber = toBlockNumber - this.maxBackfillBlocks + 1; if (lastSeenBlockNumber < minBlockNumber) { return this.getLogsInRange(filter, minBlockNumber, toBlockNumber + 1); } // Return all log events that have happened along with log events that have // been removed due to a chain reorg. const commonAncestor = yield this.getCommonAncestor(isCancelled, previousLogs); throwIfCancelled(isCancelled); // All previous logs with a block number greater than the common ancestor // were part of a re-org, so mark them as such. const removedLogs = previousLogs .filter(log => fromHex(log.blockNumber) > commonAncestor.blockNumber) .map(log => (Object.assign(Object.assign({}, log), { removed: true }))); // If no common ancestor was found, start backfill from the oldest log's // block number. const fromBlockInclusive = commonAncestor.blockNumber === Number.NEGATIVE_INFINITY ? fromHex(previousLogs[0].blockNumber) : commonAncestor.blockNumber; let addedLogs = yield this.getLogsInRange(filter, fromBlockInclusive, toBlockNumber + 1); // De-dupe any logs that were already emitted. addedLogs = addedLogs.filter(log => log && (fromHex(log.blockNumber) > commonAncestor.blockNumber || fromHex(log.logIndex) > commonAncestor.logIndex)); throwIfCancelled(isCancelled); return [...removedLogs, ...addedLogs]; }); } /** * Sets a new max backfill blocks. VISIBLE ONLY FOR TESTING. * * @internal */ setMaxBackfillBlock(newMax) { this.maxBackfillBlocks = newMax; } /** * Gets the current block number as a number. * * @private */ getBlockNumber() { return __awaiter(this, void 0, void 0, function* () { const blockNumberHex = yield this.provider.send('eth_blockNumber'); return fromHex(blockNumberHex); }); } /** * Gets all `newHead` events in the provided range. Note that the returned * heads do not include re-orged heads. Use {@link getReorgHeads} to find heads * that were part of a re-org. * * @private */ getHeadEventsInRange(fromBlockInclusive, toBlockExclusive) { return __awaiter(this, void 0, void 0, function* () { if (fromBlockInclusive >= toBlockExclusive) { return []; } const batchParts = []; for (let i = fromBlockInclusive; i < toBlockExclusive; i++) { batchParts.push({ method: 'eth_getBlockByNumber', params: [toHex(i), false] }); } // TODO: handle errors const blockHeads = yield this.provider.sendBatch(batchParts); return blockHeads.map(toNewHeadsEvent); }); } /** * Returns all heads that were part of a reorg event. * * @private */ getReorgHeads(isCancelled, previousHeads) { return __awaiter(this, void 0, void 0, function* () { const result = []; // Iterate from the most recent head backwards in order to find the first // block that was part of a re-org. for (let i = previousHeads.length - 1; i >= 0; i--) { const oldEvent = previousHeads[i]; const blockHead = yield this.getBlockByNumber(fromHex(oldEvent.number)); throwIfCancelled(isCancelled); // If the hashes match, then current head in the iteration was not re-orged. if (oldEvent.hash === blockHead.hash) { break; } result.push(toNewHeadsEvent(blockHead)); } return result.reverse(); }); } /** * Simple wrapper around `eth_getBlockByNumber` that returns the complete * block information for the provided block number. * * @private */ getBlockByNumber(blockNumber) { return __awaiter(this, void 0, void 0, function* () { return this.provider.send('eth_getBlockByNumber', [ toHex(blockNumber), false ]); }); } /** * Given a list of previous log events, finds the common block number from the * logs that matches the block head. * * This can be used to identify which logs are part of a re-org. * * Returns 1 less than the oldest log's block number if no common ancestor was found. * * @private */ getCommonAncestor(isCancelled, previousLogs) { return __awaiter(this, void 0, void 0, function* () { // Iterate from the most recent head backwards in order to find the first // block that was part of a re-org. let blockHead = yield this.getBlockByNumber(fromHex(previousLogs[previousLogs.length - 1].blockNumber)); throwIfCancelled(isCancelled); for (let i = previousLogs.length - 1; i >= 0; i--) { const oldLog = previousLogs[i]; // Ensure that updated blocks are fetched every time the log's block number // changes. if (oldLog.blockNumber !== blockHead.number) { blockHead = yield this.getBlockByNumber(fromHex(oldLog.blockNumber)); } // Since logs are ordered in ascending order, the first log that matches // the hash should be the largest logIndex. if (oldLog.blockHash === blockHead.hash) { return { blockNumber: fromHex(oldLog.blockNumber), logIndex: fromHex(oldLog.logIndex) }; } } return { blockNumber: Number.NEGATIVE_INFINITY, logIndex: Number.NEGATIVE_INFINITY }; }); } /** * Gets all `logs` events in the provided range. Note that the returned logs * do not include removed logs. * * @private */ getLogsInRange(filter, fromBlockInclusive, toBlockExclusive) { return __awaiter(this, void 0, void 0, function* () { if (fromBlockInclusive >= toBlockExclusive) { return []; } const rangeFilter = Object.assign(Object.assign({}, filter), { fromBlock: toHex(fromBlockInclusive), toBlock: toHex(toBlockExclusive - 1) }); return this.provider.send('eth_getLogs', [rangeFilter]); }); } } function toNewHeadsEvent(head) { const result = Object.assign({}, head); delete result.totalDifficulty; delete result.transactions; delete result.uncles; return result; } function dedupeNewHeads(events) { return dedupe(events, event => event.hash); } function dedupeLogs(events) { return dedupe(events, event => `${event.blockHash}/${event.logIndex}`); } function dedupe(items, getKey) { const keysSeen = new Set(); const result = []; items.forEach(item => { const key = getKey(item); if (!keysSeen.has(key)) { keysSeen.add(key); result.push(item); } }); return result; } const CANCELLED = new Error('Cancelled'); function throwIfCancelled(isCancelled) { if (isCancelled()) { throw CANCELLED; } } const HEARTBEAT_INTERVAL = 30000; const HEARTBEAT_WAIT_TIME = 10000; const BACKFILL_TIMEOUT = 60000; const BACKFILL_RETRIES = 5; /** * Subscriptions have a memory of recent events they have sent so that in the * event that they disconnect and need to backfill, they can detect re-orgs. * Keep a buffer that goes back at least these many blocks, the maximum amount * at which we might conceivably see a re-org. * * Note that while our buffer goes back this many blocks, it may contain more * than this many elements, since in the case of logs subscriptions more than * one event may be emitted for a block. */ const RETAINED_EVENT_BLOCK_COUNT = 10; /** * SDK's custom implementation fo the ethers.js's 'AlchemyWebSocketProvider'. * * Do not call this constructor directly. Instead, instantiate an instance of * {@link Alchemy} and call {@link Alchemy.config.getWebSocketProvider()}. * * @public */ class AlchemyWebSocketProvider extends WebSocketProvider { /** @internal */ constructor(config, wsConstructor) { var _a; // Normalize the API Key to a string. const apiKey = AlchemyProvider.getApiKey(config.apiKey); // Generate our own connection info with the correct endpoint URLs. const alchemyNetwork = AlchemyProvider.getAlchemyNetwork(config.network); const connection = AlchemyProvider.getAlchemyConnectionInfo(alchemyNetwork, apiKey, 'wss'); const protocol = `alchemy-sdk-${VERSION}`; // Use the provided config URL override if it exists, otherwise use the created one. const ws = new SturdyWebSocket((_a = config.url) !== null && _a !== void 0 ? _a : connection.url, protocol, { wsConstructor: wsConstructor !== null && wsConstructor !== void 0 ? wsConstructor : getWebsocketConstructor() }); // Normalize the Alchemy named network input to the network names used by // ethers. This allows the parent super constructor in JsonRpcProvider to // correctly set the network. const ethersNetwork = EthersNetwork[alchemyNetwork]; super(ws, ethersNetwork); this._events = []; // In the case of a WebSocket reconnection, all subscriptions are lost and we // create new ones to replace them, but we want to create the illusion that // the original subscriptions persist. Thus, maintain a mapping from the // "virtual" subscription ids which are visible to the consumer to the // "physical" subscription ids of the actual connections. This terminology is // borrowed from virtual and physical memory, which has a similar mapping. /** @internal */ this.virtualSubscriptionsById = new Map(); /** @internal */ this.virtualIdsByPhysicalId = new Map(); /** * The underlying ethers {@link WebSocketProvider} already handles and emits * messages. To allow backfilling, track all messages that are emitted. * * This is a field arrow function in order to preserve `this` context when * passing the method as an event listener. * * @internal */ this.handleMessage = (event) => { const message = JSON.parse(event.data); if (!isSubscriptionEvent(message)) { return; } const physicalId = message.params.subscription; const virtualId = this.virtualIdsByPhysicalId.get(physicalId); if (!virtualId) { return; } const subscription = this.virtualSubscriptionsById.get(virtualId); if (subscription.method !== 'eth_subscribe') { return; } switch (subscription.params[0]) { case 'newHeads': { const newHeadsSubscription = subscription; const newHeadsMessage = message; const { isBackfilling, backfillBuffer } = newHeadsSubscription; const { result } = newHeadsMessage.params; if (isBackfilling) { addToNewHeadsEventsBuffer(backfillBuffer, result); } else if (physicalId !== virtualId) { // In the case of a re-opened subscription, ethers will not emit the // event, so the SDK has to. this.emitAndRememberEvent(virtualId, result, getNewHeadsBlockNumber); } else { // Ethers subscription mapping will emit the event, just store it. this.rememberEvent(virtualId, result, getNewHeadsBlockNumber); } break; } case 'logs': { const logsSubscription = subscription; const logsMessage = message; const { isBackfilling, backfillBuffer } = logsSubscription; const { result } = logsMessage.params; if (isBackfilling) { addToLogsEventsBuffer(backfillBuffer, result); } else if (virtualId !== physicalId) { this.emitAndRememberEvent(virtualId, result, getLogsBlockNumber); } else { this.rememberEvent(virtualId, result, getLogsBlockNumber); } break; } default: if (physicalId !== virtualId) { // In the case of a re-opened subscription, ethers will not emit the // event, so the SDK has to. const { result } = message.params; this.emitEvent(virtualId, result); } } }; /** * When the websocket connection reopens: * * 1. Resubscribe to all existing subscriptions and start backfilling * 2. Restart heart beat. * * This is a field arrow function in order to preserve `this` context when * passing the method as an event listener. * * @internal */ this.handleReopen = () => { this.virtualIdsByPhysicalId.clear(); const { cancel, isCancelled } = makeCancelToken(); this.cancelBackfill = cancel; for (const subscription of this.virtualSubscriptionsById.values()) { void (() => __awaiter(this, void 0, void 0, function* () { try { yield this.resubscribeAndBackfill(isCancelled, subscription); } catch (error) { if (!isCancelled()) { console.error(`Error while backfilling "${subscription.params[0]}" subscription. Some events may be missing.`, error); } } }))(); } this.startHeartbeat(); }; /** * Cancels the heartbeat and any pending backfills being performed. This is * called when the websocket connection goes down or is disconnected. * * This is a field arrow function in order to preserve `this` context when * passing the method as an event listener. * * @internal */ this.stopHeartbeatAndBackfill = () => { if (this.heartbeatIntervalId != null) { clearInterval(this.heartbeatIntervalId); this.heartbeatIntervalId = undefined; } this.cancelBackfill(); }; this.apiKey = apiKey; // Start heartbeat and backfiller for the websocket connection. this.backfiller = new WebsocketBackfiller(this); this.addSocketListeners(); this.startHeartbeat(); this.cancelBackfill = noop; } /** * Overrides the `BaseProvider.getNetwork` method as implemented by ethers.js. * * This override allows the SDK to set the provider's network to values not * yet supported by ethers.js. * * @internal * @override */ static getNetwork(network) { if (typeof network === 'string' && network in CustomNetworks) { return CustomNetworks[network]; } // Call the standard ethers.js getNetwork method for other networks. return getNetwork(network); } /** * Overridden implementation of ethers that includes Alchemy based subscriptions. * * @param eventName Event to subscribe to * @param listener The listener function to call when the event is triggered. * @override * @public */ // TODO: Override `Listener` type to get type autocompletions. on(eventName, listener) { return this._addEventListener(eventName, listener, false); } /** * Overridden implementation of ethers that includes Alchemy based * subscriptions. Adds a listener to the triggered for only the next * {@link eventName} event, after which it will be removed. * * @param eventName Event to subscribe to * @param listener The listener function to call when the event is triggered. * @override * @public */ // TODO: Override `Listener` type to get type autocompletions. once(eventName, listener) { return this._addEventListener(eventName, listener, true); } /** * Removes the provided {@link listener} for the {@link eventName} event. If no * listener is provided, all listeners for the event will be removed. * * @param eventName Event to unlisten to. * @param listener The listener function to remove. * @override * @public */ off(eventName, listener) { if (isAlchemyEvent(eventName)) { return this._off(eventName, listener); } else { return super.off(eventName, listener); } } /** * Remove all listeners for the provided {@link eventName} event. If no event * is provided, all events and their listeners are removed. * * @param eventName The event to remove all listeners for. * @override * @public */ removeAllListeners(eventName) { if (eventName !== undefined && isAlchemyEvent(eventName)) { return this._removeAllListeners(eventName); } else { return super.removeAllListeners(eventName); } } /** * Returns the number of listeners for the provided {@link eventName} event. If * no event is provided, the total number of listeners for all events is returned. * * @param eventName The event to get the number of listeners for. * @public * @override */ listenerCount(eventName) { if (eventName !== undefined && isAlchemyEvent(eventName)) { return this._listenerCount(eventName); } else { return super.listenerCount(eventName); } } /** * Returns an array of listeners for the provided {@link eventName} event. If * no event is provided, all listeners will be included. * * @param eventName The event to get the listeners for. * @public * @override */ listeners(eventName) { if (eventName !== undefined && isAlchemyEvent(eventName)) { return this._listeners(eventName); } else { return super.listeners(eventName); } } /** * Overrides the method in `BaseProvider` in order to properly format the * Alchemy subscription events. * * @internal * @override */ _addEventListener(eventName, listener, once) { if (isAlchemyEvent(eventName)) { verifyAlchemyEventName(eventName); const event = new EthersEvent(getAlchemyEventTag(eventName), listener, once); this._events.push(event); this._startEvent(event); return this; } else { return super._addEventListener(eventName, listener, once); } } /** * Overrides the `_startEvent()` method in ethers.js's * {@link WebSocketProvider} to include additional alchemy methods. * * @param event * @override * @internal */ _startEvent(event) { // Check if the event type is a custom Alchemy subscription. const customLogicTypes = [...ALCHEMY_EVENT_TYPES, 'block', 'filter']; if (customLogicTypes.includes(event.type)) { this.customStartEvent(event); } else { super._startEvent(event); } } /** * Overridden from ethers.js's {@link WebSocketProvider} * * Modified in order to add mappings for backfilling. * * @internal * @override */ _subscribe(tag, param, processFunc, event) { return __awaiter(this, void 0, void 0, function* () { let subIdPromise = this._subIds[tag]; // BEGIN MODIFIED CODE const startingBlockNumber = yield this.getBlockNumber(); // END MODIFIED CODE if (subIdPromise == null) { subIdPromise = Promise.all(param).then(param => { return this.send('eth_subscribe', param); }); this._subIds[tag] = subIdPromise; } const subId = yield subIdPromise; // BEGIN MODIFIED CODE const resolvedParams = yield Promise.all(param); this.virtualSubscriptionsById.set(subId, { event: event, method: 'eth_subscribe', params: resolvedParams, startingBlockNumber, virtualId: subId, physicalId: subId, sentEvents: [], isBackfilling: false, backfillBuffer: [] }); this.virtualIdsByPhysicalId.set(subId, subId); // END MODIFIED CODE this._subs[subId] = { tag, processFunc }; }); } /** * DO NOT MODIFY. * * Original code copied over from ether.js's `BaseProvider`. * * This method is copied over directly in order to implement Alchemy's unique * subscription types. The only difference is that this method calls * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in * order to parse the Alchemy subscription event. * * @internal * @override */ emit(eventName, ...args) { if (isAlchemyEvent(eventName)) { let result = false; const stopped = []; // This line is the only modified line from the original method. const eventTag = getAlchemyEventTag(eventName); this._events = this._events.filter(event => { if (event.tag !== eventTag) { return true; } setTimeout(() => { event.listener.apply(this, args); }, 0); result = true; if (event.once) { stopped.push(event); return false; } return true; }); stopped.forEach(event => { this._stopEvent(event); }); return result; } else { return super.emit(eventName, ...args); } } /** @internal */ sendBatch(parts) { return __awaiter(this, void 0, void 0, function* () { let nextId = 0; const payload = parts.map(({ method, params }) => { return { method, params, jsonrpc: '2.0', id: `alchemy-sdk:${nextId++}` }; }); return this.sendBatchConcurrently(payload); }); } /** @override */ destroy() { this.removeSocketListeners(); this.stopHeartbeatAndBackfill(); return super.destroy(); } /** * Overrides the ether's `isCommunityResource()` method. Returns true if the * current api key is the default key. * * @override */ isCommunityResource() { return this.apiKey === DEFAULT_ALCHEMY_API_KEY; } /** * DO NOT MODIFY. * * Original code copied over from ether.js's `WebSocketProvider._stopEvent()`. * * This method is copied over directly in order to support Alchemy's * subscription type by allowing the provider to properly stop Alchemy's * subscription events. * * @internal */ _stopEvent(event) { let tag = event.tag; // START MODIFIED CODE if (ALCHEMY_EVENT_TYPES.includes(event.type)) { // There are remaining pending transaction listeners. if (this._events.filter(e => ALCHEMY_EVENT_TYPES.includes(e.type)).length) { return; } // END MODIFIED CODE } else if (event.type === 'tx') { // There are remaining transaction event listeners if (this._events.filter(e => e.type === 'tx').length) { return; } tag = 'tx'; } else if (this.listenerCount(event.event)) { // There are remaining event listeners return; } const subId = this._subIds[tag]; if (!subId) { return; } delete this._subIds[tag]; void subId.then(subId => { if (!this._subs[subId]) { return; } delete this._subs[subId]; void this.send('eth_unsubscribe', [subId]); }); } /** @internal */ addSocketListeners() { this._websocket.addEventListener('message', this.handleMessage); this._websocket.addEventListener('reopen', this.handleReopen); this._websocket.addEventListener('down', this.stopHeartbeatAndBackfill); } /** @internal */ removeSocketListeners() { this._websocket.removeEventListener('message', this.handleMessage); this._websocket.removeEventListener('reopen', this.handleReopen); this._websocket.removeEventListener('down', this.stopHeartbeatAndBackfill); } /** * Reopens the backfill based on * * @param isCancelled * @param subscription * @internal */ resubscribeAndBackfill(isCancelled, subscription) { return __awaiter(this, void 0, void 0, function* () { const { virtualId, method, params, sentEvents, backfillBuffer, startingBlockNumber } = subscription; subscription.isBackfilling = true; backfillBuffer.length = 0; try { const physicalId = yield this.send(method, params); throwIfCancelled(isCancelled); subscription.physicalId = physicalId; this.virtualIdsByPhysicalId.set(physicalId, virtualId); switch (params[0]) { case 'newHeads': { const backfillEvents = yield withBackoffRetries(() => withTimeout(this.backfiller.getNewHeadsBackfill(isCancelled, sentEvents, startingBlockNumber), BACKFILL_TIMEOUT), BACKFILL_RETRIES, () => !isCancelled()); throwIfCancelled(isCancelled); const events = dedupeNewHeads([...backfillEvents, ...backfillBuffer]); events.forEach(event => this.emitNewHeadsEvent(virtualId, event)); break; } case 'logs': { const filter = params[1] || {}; const backfillEvents = yield withBackoffRetries(() => withTimeout(this.backfiller.getLogsBackfill(isCancelled, filter, sentEvents, startingBlockNumber), BACKFILL_TIMEOUT), BACKFILL_RETRIES, () => !isCancelled()); throwIfCancelled(isCancelled); const events = dedupeLogs([...backfillEvents, ...backfillBuffer]); events.forEach(event => this.emitLogsEvent(virtualId, event)); break; } default: break; } } finally { subscription.isBackfilling = false; backfillBuffer.length = 0; } }); } /** @internal */ emitNewHeadsEvent(virtualId, result) { this.emitAndRememberEvent(virtualId, result, getNewHeadsBlockNumber); } /** @internal */ emitLogsEvent(virtualId, result) { this.emitAndRememberEvent(virtualId, result, getLogsBlockNumber); } /** * Emits an event to consumers, but also remembers it in its subscriptions's * `sentEvents` buffer so that we can detect re-orgs if the connection drops * and needs to be reconnected. * * @internal */ emitAndRememberEvent(virtualId, result, getBlockNumber) { this.rememberEvent(virtualId, result, getBlockNumber); this.emitEvent(virtualId, result); } emitEvent(virtualId, result) { const subscription = this.virtualSubscriptionsById.get(virtualId); if (!subscription) { return; } this.emitGenericEvent(subscription, result); } /** @internal */ rememberEvent(virtualId, result, getBlockNumber) { const subscription = this.virtualSubscriptionsById.get(virtualId); if (!subscription) { return; } // Web3 modifies these event objects once we pass them on (changing hex // numbers to numbers). We want the original event, so make a defensive // copy. addToPastEventsBuffer(subscription.sentEvents, Object.assign({}, result), getBlockNumber); } /** @internal */ emitGenericEvent(subscription, result) { const emitFunction = this.emitProcessFn(subscription.event); emitFunction(result); } /** * Starts a heartbeat that pings the websocket server periodically to ensure * that the connection stays open. * * @internal */ startHeartbeat() { if (this.heartbeatIntervalId != null) { return; } this.heartbeatIntervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () { try { yield withTimeout(this.send('net_version'), HEARTBEAT_WAIT_TIME); } catch (_a) { this._websocket.reconnect(); } }), HEARTBEAT_INTERVAL); } /** * This method sends the batch concurrently as individual requests rather than * as a batch, which was the original implementation. The original batch logic * is preserved in this implementation in order for faster porting. * * @param payload * @internal */ // TODO(cleanup): Refactor and remove usages of `sendBatch()`. // TODO(errors): Use allSettled() once we have more error handling. sendBatchConcurrently(payload) { return __awaiter(this, void 0, void 0, function* () { return Promise.all(payload.map(req => this.send(req.method, req.params))); }); } /** @internal */ customStartEvent(event) { if (event.type === ALCHEMY_PENDING_TRANSACTIONS_EVENT_TYPE) { const { fromAddress, toAddress, hashesOnly } = event; void this._subscribe(event.tag, [ AlchemySubscription.PENDING_TRANSACTIONS, { fromAddress, toAddress, hashesOnly } ], this.emitProcessFn(event), event); } else if (event.type === ALCHEMY_MINED_TRANSACTIONS_EVENT_TYPE) { const { addresses, includeRemoved, hashesOnly } = event; void this._subscribe(event.tag, [ AlchemySubscription.MINED_TRANSACTIONS, { addresses, includeRemoved, hashesOnly } ], this.emitProcessFn(event), event); } else if (event.type === 'block') { void this._subscribe('block', ['newHeads'], this.emitProcessFn(event), event); } else if (event.type === 'filter') { void this._subscribe(event.tag, ['logs', this._getFilter(event.filter)], this.emitProcessFn(event), event); } } /** @internal */ emitProcessFn(event) { switch (event.type) { case ALCHEMY_PENDING_TRANSACTIONS_EVENT_TYPE: return result => this.emit({ method: AlchemySubscription.PENDING_TRANSACTIONS, fromAddress: event.fromAddress, toAddress: event.toAddress, hashesOnly: event.hashesOnly }, result); case ALCHEMY_MINED_TRANSACTIONS_EVENT_TYPE: return result => this.emit({ method: AlchemySubscription.MINED_TRANSACTIONS, addresses: event.addresses, includeRemoved: event.includeRemoved, hashesOnly: event.hashesOnly }, result); case 'block': return result => { const blockNumber = BigNumber.from(result.number).toNumber(); this._emitted.block = blockNumber; this.emit('block', blockNumber); }; case 'filter': return result => { if (result.removed == null) { result.removed = false; } this.emit(event.filter, this.formatter.filterLog(result)); }; default: throw new Error('Invalid event type to `emitProcessFn()`'); } } /** * DO NOT MODIFY. * * Original code copied over from ether.js's `BaseProvider.off()`. * * This method is copied over directly in order to implement Alchemy's unique * subscription types. The only difference is that this method calls * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in * order to parse the Alchemy subscription event. * * @private */ _off(eventName, listener) { if (listener == null) { return this.removeAllListeners(eventName); } const stopped = []; let found = false; const eventTag = getAlchemyEventTag(eventName); this._events = this._events.filter(event => { if (event.tag !== eventTag || event.listener != listener) { return true; } if (found) { return true; } found = true; stopped.push(event); return false; }); stopped.forEach(event => { this._stopEvent(event); }); return this; } /** * DO NOT MODIFY. * * Original code copied over from ether.js's `BaseProvider.removeAllListeners()`. * * This method is copied over directly in order to implement Alchemy's unique * subscription types. The only difference is that this method calls * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in * order to parse the Alchemy subscription event. * * @private */ _removeAllListeners(eventName) { let stopped = []; if (eventName == null) { stopped = this._events; this._events = []; } else { const eventTag = getAlchemyEventTag(eventName); this._events = this._events.filter(event => { if (event.tag !== eventTag) { return true; } stopped.push(event); return false; }); } stopped.forEach(event => { this._stopEvent(event); }); return this; } /** * DO NOT MODIFY. * * Original code copied over from ether.js's `BaseProvider.listenerCount()`. * * This method is copied over directly in order to implement Alchemy's unique * subscription types. The only difference is that this method calls * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in * order to parse the Alchemy subscription event. * * @private */ _listenerCount(eventName) { if (!eventName) { return this._events.length; } const eventTag = getAlchemyEventTag(eventName); return this._events.filter(event => { return event.tag === eventTag; }).length; } /** * DO NOT MODIFY. * * Original code copied over from ether.js's `BaseProvider.listeners()`. * * This method is copied over directly in order to implement Alchemy's unique * subscription types. The only difference is that this method calls * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in * order to parse the Alchemy subscription event. * * @private */ _listeners(eventName) { if (eventName == null) { return this._events.map(event => event.listener); } const eventTag = getAlchemyEventTag(eventName); return this._events .filter(event => event.tag === eventTag) .map(event => event.listener); } } function getWebsocketConstructor() { return isNodeEnvironment() ? require('websocket').w3cwebsocket : WebSocket; } function isNodeEnvironment() { return (typeof process !== 'undefined' && process != null && process.versions != null && process.versions.node != null); } // TODO(cleanup): Use class variable rather than passing `isCancelled` everywhere. function makeCancelToken() { let cancelled = false; return { cancel: () => (cancelled = true), isCancelled: () => cancelled }; } // TODO(cleanup): replace with SDK's backoff implementation const MIN_RETRY_DELAY = 1000; const RETRY_BACKOFF_FACTOR = 2; const MAX_RETRY_DELAY = 30000; function withBackoffRetries(f, retryCount, shouldRetry = () => true) { return __awaiter(this, void 0, void 0, function* () { let nextWaitTime = 0; let i = 0; while (true) { try { return yield f(); } catch (error) { i++; if (i >= retryCount || !shouldRetry(error)) { throw error; } yield delay(nextWaitTime); if (!shouldRetry(error)) { throw error; } nextWaitTime = nextWaitTime === 0 ? MIN_RETRY_DELAY : Math.min(MAX_RETRY_DELAY, RETRY_BACKOFF_FACTOR * nextWaitTime); } } }); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function withTimeout(promise, ms) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms)) ]); } function getNewHeadsBlockNumber(event) { return fromHex(event.number); } function getLogsBlockNumber(event) { return fromHex(event.blockNumber); } function isResponse(message) { return (Array.isArray(message) || (message.jsonrpc === '2.0' && message.id !== undefined)); } function isSubscriptionEvent(message) { return !isResponse(message); } function addToNewHeadsEventsBuffer(pastEvents, event) { addToPastEventsBuffer(pastEvents, event, getNewHeadsBlockNumber); } function addToLogsEventsBuffer(pastEvents, event) { addToPastEventsBuffer(pastEvents, event, getLogsBlockNumber); } /** * Adds a new event to an array of events, evicting any events which are so old * that they will no longer feasibly be part of a reorg. */ function addToPastEventsBuffer(pastEvents, event, getBlockNumber) { const currentBlockNumber = getBlockNumber(event); // Find first index of an event recent enough to retain, then drop everything // at a lower index. const firstGoodIndex = pastEvents.findIndex(e => getBlockNumber(e) > currentBlockNumber - RETAINED_EVENT_BLOCK_COUNT); if (firstGoodIndex === -1) { pastEvents.length = 0; } else { pastEvents.splice(0, firstGoodIndex); } pastEvents.push(event); } export { AlchemyWebSocketProvider }; //# sourceMappingURL=alchemy-websocket-provider-948c1ea8.js.map