UNPKG

@atomiqlabs/chain-starknet

Version:
535 lines (534 loc) 25.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StarknetChainEventsBrowser = void 0; const base_1 = require("@atomiqlabs/base"); const Utils_1 = require("../../utils/Utils"); const starknet_1 = require("starknet"); const sha2_1 = require("@noble/hashes/sha2"); const buffer_1 = require("buffer"); const PROCESSED_EVENTS_BACKLOG = 5000; const LOGS_SLIDING_WINDOW = 60; /** * Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose * out on some events if the network is unreliable, front-end systems should take this into consideration and not * rely purely on events * * @category Events */ class StarknetChainEventsBrowser { constructor(chainInterface, starknetSwapContract, starknetSpvVaultContract, pollIntervalSeconds = 5) { this.eventsProcessing = {}; this.processedEvents = new Set(); /** * @internal */ this.listeners = []; /** * @internal */ this.logger = (0, Utils_1.getLogger)("StarknetChainEventsBrowser: "); /** * @internal */ this.stopped = true; /** * @internal */ this.wsStarted = false; this.Chain = chainInterface; this.wsChannel = chainInterface.wsChannel; this.provider = chainInterface.provider; this.starknetSwapContract = starknetSwapContract; this.starknetSpvVaultContract = starknetSpvVaultContract; this.pollIntervalSeconds = pollIntervalSeconds; } /** * * @param event * @private */ getEventFingerprint(event) { const eventData = buffer_1.Buffer.concat([ ...event.keys.map(value => (0, Utils_1.bigNumberishToBuffer)(value, 32)), ...event.data.map(value => (0, Utils_1.bigNumberishToBuffer)(value, 32)) ]); const fingerprint = buffer_1.Buffer.from((0, sha2_1.sha256)(eventData)); return event.txHash + ":" + fingerprint.toString("hex"); } /** * * @param event * @private */ addProcessedEvent(event) { this.processedEvents.add(this.getEventFingerprint(event)); if (this.processedEvents.size > PROCESSED_EVENTS_BACKLOG) this.processedEvents.delete(this.processedEvents.keys().next().value); } /** * * @param eventOrFingerprint * @private */ isEventProcessed(eventOrFingerprint) { const eventFingerprint = typeof (eventOrFingerprint) === "string" ? eventOrFingerprint : this.getEventFingerprint(eventOrFingerprint); return this.processedEvents.has(eventFingerprint); } /** * Returns async getter for fetching on-demand initialize event swap data * * @param event * @param claimHandler * @private * @returns {() => Promise<StarknetSwapData>} getter to be passed to InitializeEvent constructor */ getSwapDataGetter(event, claimHandler) { return async () => { const trace = await this.Chain.Transactions.traceTransaction(event.txHash, event.blockHash); if (trace == null) return null; return this.starknetSwapContract.findInitSwapData(trace, event.params.escrow_hash, claimHandler); }; } /** * * @param event * @private */ parseInitializeEvent(event) { const escrowHashBuffer = (0, Utils_1.bigNumberishToBuffer)(event.params.escrow_hash, 32); const escrowHash = escrowHashBuffer.toString("hex"); const claimHandlerHex = (0, Utils_1.toHex)(event.params.claim_handler); const claimHandler = this.starknetSwapContract.claimHandlersByAddress[claimHandlerHex]; if (claimHandler == null) { this.logger.warn("parseInitializeEvent(" + escrowHash + "): Unknown claim handler with claim: " + claimHandlerHex); return null; } const swapType = claimHandler.getType(); this.logger.debug("InitializeEvent claimHash: " + (0, Utils_1.toHex)(event.params.claim_data) + " escrowHash: " + escrowHash); return new base_1.InitializeEvent(escrowHash, swapType, (0, Utils_1.onceAsync)(this.getSwapDataGetter(event, claimHandler))); } /** * * @param event * @private */ parseRefundEvent(event) { const escrowHashBuffer = (0, Utils_1.bigNumberishToBuffer)(event.params.escrow_hash, 32); const escrowHash = escrowHashBuffer.toString("hex"); this.logger.debug("RefundEvent claimHash: " + (0, Utils_1.toHex)(event.params.claim_data) + " escrowHash: " + escrowHash); return new base_1.RefundEvent(escrowHash); } /** * * @param event * @private */ parseClaimEvent(event) { const escrowHashBuffer = (0, Utils_1.bigNumberishToBuffer)(event.params.escrow_hash, 32); const escrowHash = escrowHashBuffer.toString("hex"); const claimHandlerHex = (0, Utils_1.toHex)(event.params.claim_handler); const claimHandler = this.starknetSwapContract.claimHandlersByAddress[claimHandlerHex]; if (claimHandler == null) { this.logger.warn("parseClaimEvent(" + escrowHash + "): Unknown claim handler with claim: " + claimHandlerHex); return null; } const witnessResult = claimHandler.parseWitnessResult(event.params.witness_result); this.logger.debug("ClaimEvent claimHash: " + (0, Utils_1.toHex)(event.params.claim_data) + " witnessResult: " + witnessResult + " escrowHash: " + escrowHash); return new base_1.ClaimEvent(escrowHash, witnessResult); } /** * * @param event * @private */ parseSpvOpenEvent(event) { const owner = (0, Utils_1.toHex)(event.params.owner); const vaultId = (0, Utils_1.toBigInt)(event.params.vault_id); const btcTxId = (0, Utils_1.bigNumberishToBuffer)(event.params.btc_tx_hash, 32).reverse().toString("hex"); const vout = Number((0, Utils_1.toBigInt)(event.params.vout)); this.logger.debug("SpvOpenEvent owner: " + owner + " vaultId: " + vaultId + " utxo: " + btcTxId + ":" + vout); return new base_1.SpvVaultOpenEvent(owner, vaultId, btcTxId, vout); } /** * * @param event * @private */ parseSpvDepositEvent(event) { const owner = (0, Utils_1.toHex)(event.params.owner); const vaultId = (0, Utils_1.toBigInt)(event.params.vault_id); const amounts = [(0, Utils_1.toBigInt)(event.params.amounts["0"]), (0, Utils_1.toBigInt)(event.params.amounts["1"])]; const depositCount = Number((0, Utils_1.toBigInt)(event.params.deposit_count)); this.logger.debug("SpvDepositEvent owner: " + owner + " vaultId: " + vaultId + " depositCount: " + depositCount + " amounts: ", amounts); return new base_1.SpvVaultDepositEvent(owner, vaultId, amounts, depositCount); } /** * * @param event * @private */ parseSpvFrontEvent(event) { const owner = (0, Utils_1.toHex)(event.params.owner); const vaultId = (0, Utils_1.toBigInt)(event.params.vault_id); const btcTxId = (0, Utils_1.bigNumberishToBuffer)(event.params.btc_tx_hash, 32).reverse().toString("hex"); const recipient = (0, Utils_1.toHex)(event.params.recipient); const executionHash = (0, Utils_1.toHex)(event.params.execution_hash); const amounts = [(0, Utils_1.toBigInt)(event.params.amounts["0"]), (0, Utils_1.toBigInt)(event.params.amounts["1"])]; const frontingAddress = (0, Utils_1.toHex)(event.params.caller); this.logger.debug("SpvFrontEvent owner: " + owner + " vaultId: " + vaultId + " btcTxId: " + btcTxId + " recipient: " + recipient + " frontedBy: " + frontingAddress + " amounts: ", amounts); return new base_1.SpvVaultFrontEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, frontingAddress); } /** * * @param event * @private */ parseSpvClaimEvent(event) { const owner = (0, Utils_1.toHex)(event.params.owner); const vaultId = (0, Utils_1.toBigInt)(event.params.vault_id); const btcTxId = (0, Utils_1.bigNumberishToBuffer)(event.params.btc_tx_hash, 32).reverse().toString("hex"); const recipient = (0, Utils_1.toHex)(event.params.recipient); const executionHash = (0, Utils_1.toHex)(event.params.execution_hash); const amounts = [(0, Utils_1.toBigInt)(event.params.amounts["0"]), (0, Utils_1.toBigInt)(event.params.amounts["1"])]; const caller = (0, Utils_1.toHex)(event.params.caller); const frontingAddress = (0, Utils_1.toHex)(event.params.fronting_address); const withdrawCount = Number((0, Utils_1.toBigInt)(event.params.withdraw_count)); this.logger.debug("SpvClaimEvent owner: " + owner + " vaultId: " + vaultId + " btcTxId: " + btcTxId + " withdrawCount: " + withdrawCount + " recipient: " + recipient + " frontedBy: " + frontingAddress + " claimedBy: " + caller + " amounts: ", amounts); return new base_1.SpvVaultClaimEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, caller, frontingAddress, withdrawCount); } /** * * @param event * @private */ parseSpvCloseEvent(event) { const owner = (0, Utils_1.toHex)(event.params.owner); const vaultId = (0, Utils_1.toBigInt)(event.params.vault_id); const btcTxId = (0, Utils_1.bigNumberishToBuffer)(event.params.btc_tx_hash, 32).reverse().toString("hex"); const error = (0, Utils_1.bigNumberishToBuffer)(event.params.error).toString(); return new base_1.SpvVaultCloseEvent(owner, vaultId, btcTxId, error); } /** * Processes event as received from the chain, parses it & calls event listeners * * @param events * @param currentBlockNumber * @param currentBlockTimestamp * @private */ async processEvents(events, currentBlockNumber, currentBlockTimestamp) { const blockTimestampsCache = {}; const getBlockTimestamp = async (blockNumber) => { //Use current timestamp for events without block height (probably pre-confirmed) if (blockNumber == null) return Math.floor(Date.now() / 1000); if (currentBlockTimestamp != null && blockNumber === currentBlockNumber) return currentBlockTimestamp; const blockNumberString = blockNumber.toString(); blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] = await this.Chain.Blocks.getBlockTime(blockNumber)); return blockTimestampsCache[blockNumberString]; }; for (let event of events) { const eventIdentifier = this.getEventFingerprint(event); if (this.isEventProcessed(eventIdentifier)) { this.logger.debug("processEvents(): skipping already processed event: " + eventIdentifier); continue; } let parsedEvent; switch (event.name) { case "escrow_manager::events::Claim": parsedEvent = this.parseClaimEvent(event); break; case "escrow_manager::events::Refund": parsedEvent = this.parseRefundEvent(event); break; case "escrow_manager::events::Initialize": parsedEvent = this.parseInitializeEvent(event); break; case "spv_swap_vault::events::Opened": parsedEvent = this.parseSpvOpenEvent(event); break; case "spv_swap_vault::events::Deposited": parsedEvent = this.parseSpvDepositEvent(event); break; case "spv_swap_vault::events::Fronted": parsedEvent = this.parseSpvFrontEvent(event); break; case "spv_swap_vault::events::Claimed": parsedEvent = this.parseSpvClaimEvent(event); break; case "spv_swap_vault::events::Closed": parsedEvent = this.parseSpvCloseEvent(event); break; } if (this.eventsProcessing[eventIdentifier] != null) { this.logger.debug("processEvents(): awaiting event that is currently processing: " + eventIdentifier); await this.eventsProcessing[eventIdentifier]; continue; } const promise = (async () => { if (parsedEvent == null) return; //We are not trusting pre-confs for events, so this shall never happen if (event.blockNumber == null) throw new Error("Event block number cannot be null!"); const timestamp = await getBlockTimestamp(event.blockNumber); parsedEvent.meta = { blockTime: timestamp, txId: event.txHash, timestamp //Maybe deprecated }; const eventsArr = [parsedEvent]; for (let listener of this.listeners) { await listener(eventsArr); } this.addProcessedEvent(event); })(); this.eventsProcessing[eventIdentifier] = promise; try { await promise; delete this.eventsProcessing[eventIdentifier]; } catch (e) { delete this.eventsProcessing[eventIdentifier]; throw e; } } } /** * * @param currentBlock * @param lastTxHash * @param lastBlockNumber * @private */ async checkEventsEcrowManager(currentBlock, lastTxHash, lastBlockNumber) { const currentBlockNumber = currentBlock.block_number; lastBlockNumber ?? (lastBlockNumber = currentBlockNumber); if (currentBlockNumber < lastBlockNumber) { this.logger.warn(`checkEventsEscrowManager(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`); return { lastTxHash, lastBlockNumber }; } // this.logger.debug("checkEvents(EscrowManager): Requesting logs: "+logStartHeight+"...pending"); let events = await this.starknetSwapContract._Events.getContractBlockEvents(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], [], lastBlockNumber, null); if (lastTxHash != null) { const latestProcessedEventIndex = (0, Utils_1.findLastIndex)(events, val => val.txHash === lastTxHash); if (latestProcessedEventIndex !== -1) { events.splice(0, latestProcessedEventIndex + 1); this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: " + events.length); } } if (events.length > 0) { await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp); const lastProcessed = events[events.length - 1]; lastTxHash = lastProcessed.txHash; const lastProcessedWithBlockHeightIndex = (0, Utils_1.findLastIndex)(events, val => val.blockNumber != null); if (lastProcessedWithBlockHeightIndex !== -1) { const lastProcessedWithBlockHeight = events[lastProcessedWithBlockHeightIndex]; if (lastProcessedWithBlockHeight.blockNumber > lastBlockNumber) lastBlockNumber = lastProcessedWithBlockHeight.blockNumber; } } else if (currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) { lastTxHash = undefined; lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW; } return { lastTxHash, lastBlockNumber }; } async checkEventsSpvVaults(currentBlock, lastTxHash, lastBlockNumber) { const currentBlockNumber = currentBlock.block_number; lastBlockNumber ?? (lastBlockNumber = currentBlockNumber); if (currentBlockNumber < lastBlockNumber) { this.logger.warn(`checkEventsSpvVaults(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`); return { lastTxHash, lastBlockNumber }; } // this.logger.debug("checkEvents(SpvVaults): Requesting logs: "+logStartHeight+"...pending"); let events = await this.starknetSpvVaultContract._Events.getContractBlockEvents(["spv_swap_vault::events::Opened", "spv_swap_vault::events::Deposited", "spv_swap_vault::events::Closed", "spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"], [], lastBlockNumber, null); if (lastTxHash != null) { const latestProcessedEventIndex = (0, Utils_1.findLastIndex)(events, val => val.txHash === lastTxHash); if (latestProcessedEventIndex !== -1) { events.splice(0, latestProcessedEventIndex + 1); this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: " + events.length); } } if (events.length > 0) { await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp); const lastProcessed = events[events.length - 1]; lastTxHash = lastProcessed.txHash; const lastProcessedWithBlockHeightIndex = (0, Utils_1.findLastIndex)(events, val => val.blockNumber != null); if (lastProcessedWithBlockHeightIndex !== -1) { const lastProcessedWithBlockHeight = events[lastProcessedWithBlockHeightIndex]; if (lastProcessedWithBlockHeight.blockNumber > lastBlockNumber) lastBlockNumber = lastProcessedWithBlockHeight.blockNumber; } } else if (currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) { lastTxHash = undefined; lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW; } return { lastTxHash, lastBlockNumber }; } /** * @inheritDoc */ async poll(lastState) { lastState ?? (lastState = []); const currentBlock = await this.Chain.Blocks.getBlock(starknet_1.BlockTag.LATEST); const resultEscrow = await this.checkEventsEcrowManager(currentBlock, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber); const resultSpvVault = await this.checkEventsSpvVaults(currentBlock, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber); return [ resultEscrow, resultSpvVault ]; } /** * Sets up event handlers listening for swap events over websocket * * @internal */ async setupPoll(lastState, saveLatestProcessedBlockNumber) { let func; func = async () => { await this.poll(lastState).then(newState => { lastState = newState; if (saveLatestProcessedBlockNumber != null) return saveLatestProcessedBlockNumber(newState); }).catch(e => { this.logger.error("setupPoll(): Failed to fetch starknet log: ", e); }); if (this.stopped) return; this.timeout = setTimeout(func, this.pollIntervalSeconds * 1000); }; await func(); } /** * * @private */ async subscribeWsEscrowEvents() { let subscription; do { try { subscription = await this.wsChannel.subscribeEvents({ fromAddress: this.starknetSwapContract.contract.address, keys: this.starknetSwapContract._Events.toFilter(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], []), finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2 }); } catch (e) { this.logger.error("subscribeWsEscrowEvents(): Failed to subscribe to escrow events, retrying in 10 seconds..."); await new Promise(resolve => setTimeout(resolve, 10 * 1000)); } } while (subscription == null); subscription.on((event) => { const parsedEvents = this.starknetSwapContract._Events.toStarknetAbiEvents([event]); this.processEvents(parsedEvents, event.block_number).catch(e => { console.error(`WS: EscrowContract: Failed to process event ${parsedEvents[0].txHash}:${parsedEvents[0].name}: `, e); }); }); this.escrowContractSubscription = subscription; this.logger.debug("subscribeWsEscrowEvents(): Successfully subscribed to escrow contract WS events"); } /** * * @private */ async subscribeWsSpvVaultEvents() { let subscription; do { try { subscription = await this.wsChannel.subscribeEvents({ fromAddress: this.starknetSpvVaultContract.contract.address, keys: this.starknetSpvVaultContract._Events.toFilter(["spv_swap_vault::events::Opened", "spv_swap_vault::events::Deposited", "spv_swap_vault::events::Closed", "spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"], []), finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2 }); } catch (e) { this.logger.error("subscribeWsSpvVaultEvents(): Failed to subscribe to spv vault events, retrying in 10 seconds..."); await new Promise(resolve => setTimeout(resolve, 10 * 1000)); } } while (subscription == null); subscription.on((event) => { const parsedEvents = this.starknetSpvVaultContract._Events.toStarknetAbiEvents([event]); this.processEvents(parsedEvents, event.block_number).catch(e => { console.error(`WS: SpvVaultContract: Failed to process event ${parsedEvents[0].txHash}:${parsedEvents[0].name}: `, e); }); }); this.spvVaultContractSubscription = subscription; this.logger.debug("subscribeWsSpvVaultEvents(): Successfully subscribed to spv vault contract WS events"); } /** * @internal */ async setupWebsocket() { if (this.wsChannel == null) throw new Error("Tried to setup websocket subscription on a provider without WS"); this.wsStarted = true; this.wsChannel.on("open", () => { this.logger.info("setupWebsocket(): Websocket connection opened!"); }); this.wsChannel.on("close", () => { this.logger.warn("setupWebsocket(): Websocket connection closed!"); }); this.wsChannel.on("error", (err) => { this.logger.error("setupWebsocket(): Websocket connection error: ", err); }); //We don't await these, since they might block indefinitely this.subscribeWsEscrowEvents(); this.subscribeWsSpvVaultEvents(); } /** * @inheritDoc */ async init(noAutomaticPoll) { if (noAutomaticPoll) return; this.stopped = false; if (this.wsChannel != null) { this.logger.debug("init(): WS channel detected, setting up websocket-based subscription!"); await this.setupWebsocket(); } else { this.logger.debug("init(): Setting up HTTP polling events subscription!"); await this.setupPoll(); } } /** * Stops all event subscriptions and timers */ async stop() { this.stopped = true; if (this.timeout != null) clearTimeout(this.timeout); if (this.wsStarted) { if (this.escrowContractSubscription != null) await this.escrowContractSubscription.unsubscribe(); if (this.spvVaultContractSubscription != null) await this.spvVaultContractSubscription.unsubscribe(); this.wsStarted = false; } } /** * @inheritDoc */ registerListener(cbk) { this.listeners.push(cbk); } /** * @inheritDoc */ unregisterListener(cbk) { const index = this.listeners.indexOf(cbk); if (index >= 0) { this.listeners.splice(index, 1); return true; } return false; } } exports.StarknetChainEventsBrowser = StarknetChainEventsBrowser;