@atomiqlabs/chain-starknet
Version:
Starknet specific base implementation
720 lines (660 loc) • 30.3 kB
text/typescript
import {
ChainEvent,
ChainEvents,
ChainSwapType,
ClaimEvent,
EventListener,
InitializeEvent,
RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent
} from "@atomiqlabs/base";
import {StarknetSwapData} from "../swaps/StarknetSwapData";
import {
bigNumberishToBuffer,
bytes31SpanToBuffer, findLastIndex,
getLogger,
onceAsync,
toBigInt,
toHex
} from "../../utils/Utils";
import {StarknetSwapContract} from "../swaps/StarknetSwapContract";
import {
BigNumberish,
BlockTag,
hash,
Provider,
SubscriptionStarknetEventsEvent,
TransactionFinalityStatus,
WebSocketChannel
} from "starknet";
import {StarknetAbiEvent} from "../contract/modules/StarknetContractEvents";
import {EscrowManagerAbiType} from "../swaps/EscrowManagerAbi";
import {ExtractAbiFunctionNames} from "abi-wan-kanabi/dist/kanabi";
import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";
import {StarknetSpvVaultContract} from "../spv_swap/StarknetSpvVaultContract";
import {StarknetChainInterface} from "../chain/StarknetChainInterface";
import {SpvVaultContractAbiType} from "../spv_swap/SpvVaultContractAbi";
import {sha256} from "@noble/hashes/sha2";
import {Buffer} from "buffer";
const PROCESSED_EVENTS_BACKLOG = 5000;
const LOGS_SLIDING_WINDOW = 60;
/**
* Current state of the starknet event listener, contains the last processed
* block number and transaction hash of the last processed event
*
* @category Events
*/
export type StarknetEventListenerState = {
/**
* Block number of the last processed event
*/
lastBlockNumber: number,
/**
* Transaction hash of the last processed event
*/
lastTxHash?: string
};
/**
* 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
*/
export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData, StarknetEventListenerState[]> {
private eventsProcessing: {
[eventFingerprint: string]: Promise<void>
} = {};
private processedEvents: Set<string> = new Set();
/**
* @internal
*/
protected readonly Chain: StarknetChainInterface;
/**
* @internal
*/
protected readonly listeners: EventListener<StarknetSwapData>[] = [];
/**
* @internal
*/
protected readonly wsChannel?: WebSocketChannel;
/**
* @internal
*/
protected readonly provider: Provider;
/**
* @internal
*/
protected readonly starknetSwapContract: StarknetSwapContract;
/**
* @internal
*/
protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
/**
* @internal
*/
protected readonly logger = getLogger("StarknetChainEventsBrowser: ");
/**
* @internal
*/
protected escrowContractSubscription?: SubscriptionStarknetEventsEvent;
/**
* @internal
*/
protected spvVaultContractSubscription?: SubscriptionStarknetEventsEvent;
/**
* @internal
*/
protected stopped: boolean = true;
/**
* @internal
*/
protected pollIntervalSeconds: number;
private timeout: any;
constructor(
chainInterface: StarknetChainInterface,
starknetSwapContract: StarknetSwapContract,
starknetSpvVaultContract: StarknetSpvVaultContract,
pollIntervalSeconds: number = 5
) {
this.Chain = chainInterface;
this.wsChannel = chainInterface.wsChannel;
this.provider = chainInterface.provider;
this.starknetSwapContract = starknetSwapContract;
this.starknetSpvVaultContract = starknetSpvVaultContract;
this.pollIntervalSeconds = pollIntervalSeconds;
}
/**
*
* @param event
* @private
*/
private getEventFingerprint(event: {keys: string[], data: string[], txHash: string}): string {
const eventData = Buffer.concat([
...event.keys.map(value => bigNumberishToBuffer(value, 32)),
...event.data.map(value => bigNumberishToBuffer(value, 32))
]);
const fingerprint = Buffer.from(sha256(eventData));
return event.txHash+":"+fingerprint.toString("hex");
}
/**
*
* @param event
* @private
*/
private addProcessedEvent(event: {keys: string[], data: string[], txHash: string}) {
this.processedEvents.add(this.getEventFingerprint(event));
if(this.processedEvents.size > PROCESSED_EVENTS_BACKLOG) this.processedEvents.delete(this.processedEvents.keys().next().value!);
}
/**
*
* @param eventOrFingerprint
* @private
*/
private isEventProcessed(eventOrFingerprint: {keys: string[], data: string[], txHash: string} | string): boolean {
const eventFingerprint: string = 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
*/
private getSwapDataGetter(
event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Initialize">,
claimHandler: IClaimHandler<any, any>
): () => Promise<StarknetSwapData | null> {
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
*/
private parseInitializeEvent(
event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Initialize">
): InitializeEvent<StarknetSwapData> | null {
const escrowHashBuffer = bigNumberishToBuffer(event.params.escrow_hash, 32);
const escrowHash = escrowHashBuffer.toString("hex");
const claimHandlerHex = 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: ChainSwapType = claimHandler.getType();
this.logger.debug("InitializeEvent claimHash: "+toHex(event.params.claim_data)+" escrowHash: "+escrowHash);
return new InitializeEvent<StarknetSwapData>(
escrowHash,
swapType,
onceAsync<StarknetSwapData | null>(this.getSwapDataGetter(event, claimHandler))
);
}
/**
*
* @param event
* @private
*/
private parseRefundEvent(
event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Refund">
): RefundEvent<StarknetSwapData> {
const escrowHashBuffer = bigNumberishToBuffer(event.params.escrow_hash, 32);
const escrowHash = escrowHashBuffer.toString("hex");
this.logger.debug("RefundEvent claimHash: "+toHex(event.params.claim_data)+" escrowHash: "+escrowHash);
return new RefundEvent<StarknetSwapData>(escrowHash);
}
/**
*
* @param event
* @private
*/
private parseClaimEvent(
event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Claim">
): ClaimEvent<StarknetSwapData> | null {
const escrowHashBuffer = bigNumberishToBuffer(event.params.escrow_hash, 32);
const escrowHash = escrowHashBuffer.toString("hex");
const claimHandlerHex = 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: "+toHex(event.params.claim_data)+
" witnessResult: "+witnessResult+" escrowHash: "+escrowHash);
return new ClaimEvent<StarknetSwapData>(escrowHash, witnessResult);
}
/**
*
* @param event
* @private
*/
private parseSpvOpenEvent(
event: StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Opened">
): SpvVaultOpenEvent {
const owner = toHex(event.params.owner);
const vaultId = toBigInt(event.params.vault_id);
const btcTxId = bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex");
const vout = Number(toBigInt(event.params.vout));
this.logger.debug("SpvOpenEvent owner: "+owner+" vaultId: "+vaultId+" utxo: "+btcTxId+":"+vout);
return new SpvVaultOpenEvent(owner, vaultId, btcTxId, vout);
}
/**
*
* @param event
* @private
*/
private parseSpvDepositEvent(
event: StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Deposited">
): SpvVaultDepositEvent {
const owner = toHex(event.params.owner);
const vaultId = toBigInt(event.params.vault_id);
const amounts = [toBigInt(event.params.amounts["0"] as BigNumberish), toBigInt(event.params.amounts["1"] as BigNumberish)];
const depositCount = Number(toBigInt(event.params.deposit_count));
this.logger.debug("SpvDepositEvent owner: "+owner+" vaultId: "+vaultId+" depositCount: "+depositCount+" amounts: ", amounts);
return new SpvVaultDepositEvent(owner, vaultId, amounts, depositCount);
}
/**
*
* @param event
* @private
*/
private parseSpvFrontEvent(
event: StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Fronted">
): SpvVaultFrontEvent {
const owner = toHex(event.params.owner);
const vaultId = toBigInt(event.params.vault_id);
const btcTxId = bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex");
const recipient = toHex(event.params.recipient);
const executionHash = toHex(event.params.execution_hash);
const amounts = [toBigInt(event.params.amounts["0"] as BigNumberish), toBigInt(event.params.amounts["1"] as BigNumberish)];
const frontingAddress = toHex(event.params.caller);
this.logger.debug("SpvFrontEvent owner: "+owner+" vaultId: "+vaultId+" btcTxId: "+btcTxId+
" recipient: "+recipient+" frontedBy: "+frontingAddress+" amounts: ", amounts);
return new SpvVaultFrontEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, frontingAddress);
}
/**
*
* @param event
* @private
*/
private parseSpvClaimEvent(
event: StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Claimed">
): SpvVaultClaimEvent {
const owner = toHex(event.params.owner);
const vaultId = toBigInt(event.params.vault_id);
const btcTxId = bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex");
const recipient = toHex(event.params.recipient);
const executionHash = toHex(event.params.execution_hash);
const amounts = [toBigInt(event.params.amounts["0"] as BigNumberish), toBigInt(event.params.amounts["1"] as BigNumberish)];
const caller = toHex(event.params.caller);
const frontingAddress = toHex(event.params.fronting_address);
const withdrawCount = Number(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 SpvVaultClaimEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, caller, frontingAddress, withdrawCount);
}
/**
*
* @param event
* @private
*/
private parseSpvCloseEvent(
event: StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Closed">
): SpvVaultCloseEvent {
const owner = toHex(event.params.owner);
const vaultId = toBigInt(event.params.vault_id);
const btcTxId = bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex");
const error = bigNumberishToBuffer(event.params.error).toString();
return new SpvVaultCloseEvent(owner, vaultId, btcTxId, error);
}
/**
* Processes event as received from the chain, parses it & calls event listeners
*
* @param events
* @param currentBlockNumber
* @param currentBlockTimestamp
* @private
*/
private async processEvents(
events : (StarknetAbiEvent<
EscrowManagerAbiType,
"escrow_manager::events::Initialize" | "escrow_manager::events::Refund" | "escrow_manager::events::Claim"
> | StarknetAbiEvent<
SpvVaultContractAbiType,
"spv_swap_vault::events::Opened" | "spv_swap_vault::events::Deposited" | "spv_swap_vault::events::Fronted" | "spv_swap_vault::events::Claimed" | "spv_swap_vault::events::Closed"
>)[],
currentBlockNumber?: number,
currentBlockTimestamp?: number
) {
const blockTimestampsCache: {[blockNumber: string]: number} = {};
const getBlockTimestamp: (blockNumber?: number) => Promise<number> = async (blockNumber?: number)=> {
//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] ??= 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: ChainEvent<StarknetSwapData> | null;
switch(event.name) {
case "escrow_manager::events::Claim":
parsedEvent = this.parseClaimEvent(event as any);
break;
case "escrow_manager::events::Refund":
parsedEvent = this.parseRefundEvent(event as any);
break;
case "escrow_manager::events::Initialize":
parsedEvent = this.parseInitializeEvent(event as any);
break;
case "spv_swap_vault::events::Opened":
parsedEvent = this.parseSpvOpenEvent(event as any);
break;
case "spv_swap_vault::events::Deposited":
parsedEvent = this.parseSpvDepositEvent(event as any);
break;
case "spv_swap_vault::events::Fronted":
parsedEvent = this.parseSpvFrontEvent(event as any);
break;
case "spv_swap_vault::events::Claimed":
parsedEvent = this.parseSpvClaimEvent(event as any);
break;
case "spv_swap_vault::events::Closed":
parsedEvent = this.parseSpvCloseEvent(event as any);
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
} as any;
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
*/
private async checkEventsEcrowManager(
currentBlock: {timestamp: number, block_number: number},
lastTxHash?: string,
lastBlockNumber?: number
): Promise<StarknetEventListenerState> {
const currentBlockNumber: number = currentBlock.block_number;
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 = 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 = 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};
}
private async checkEventsSpvVaults(currentBlock: {timestamp: number, block_number: number}, lastTxHash?: string, lastBlockNumber?: number): Promise<StarknetEventListenerState> {
const currentBlockNumber: number = currentBlock.block_number;
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 = 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 = 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?: StarknetEventListenerState[]): Promise<StarknetEventListenerState[]> {
lastState ??= [];
const currentBlock = await this.Chain.Blocks.getBlock(BlockTag.LATEST);
const resultEscrow = await this.checkEventsEcrowManager(currentBlock as any, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
const resultSpvVault = await this.checkEventsSpvVaults(currentBlock as any, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
return [
resultEscrow,
resultSpvVault
];
}
/**
* Sets up event handlers listening for swap events over websocket
*
* @internal
*/
protected async setupPoll(
lastState?: StarknetEventListenerState[],
saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>
) {
let func: () => Promise<void>;
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();
}
/**
* @internal
*/
protected wsStarted: boolean = false;
/**
*
* @private
*/
private async subscribeWsEscrowEvents() {
let subscription: SubscriptionStarknetEventsEvent | undefined;
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: 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<
"escrow_manager::events::Initialize" | "escrow_manager::events::Claim" | "escrow_manager::events::Refund"
>([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
*/
private async subscribeWsSpvVaultEvents() {
let subscription: SubscriptionStarknetEventsEvent | undefined;
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: 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<
"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"
>([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
*/
protected 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?: boolean): Promise<void> {
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(): Promise<void> {
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: EventListener<StarknetSwapData>): void {
this.listeners.push(cbk);
}
/**
* @inheritDoc
*/
unregisterListener(cbk: EventListener<StarknetSwapData>): boolean {
const index = this.listeners.indexOf(cbk);
if(index>=0) {
this.listeners.splice(index, 1);
return true;
}
return false;
}
}