@atomiqlabs/chain-starknet
Version:
Starknet specific base implementation
535 lines (534 loc) • 25.4 kB
JavaScript
"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;