@atomiqlabs/chain-evm 1.0.0-dev.48 → 1.0.0-dev.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,4 +10,5 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import { EVMSwapContract } from "../../evm/swaps/EVMSwapContract";
11
11
  import { EVMBtcRelay } from "../../evm/btcrelay/EVMBtcRelay";
12
12
  import { EVMSpvVaultContract } from "../../evm/spv_swap/EVMSpvVaultContract";
13
- export type BotanixChainType = ChainType<"BOTANIX", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, EVMSwapContract<"BOTANIX">, EVMChainInterface<"BOTANIX", 3636>, EVMChainEventsBrowser, EVMBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, EVMSpvVaultContract<"BOTANIX">>;
13
+ import { EVMChainEventsBrowserWS } from "../../evm/events/EVMChainEventsBrowserWS";
14
+ export type BotanixChainType = ChainType<"BOTANIX", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, EVMSwapContract<"BOTANIX">, EVMChainInterface<"BOTANIX", 3636>, EVMChainEventsBrowser | EVMChainEventsBrowserWS, EVMBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, EVMSpvVaultContract<"BOTANIX">>;
@@ -12,6 +12,7 @@ const EVMChainEventsBrowser_1 = require("../../evm/events/EVMChainEventsBrowser"
12
12
  const EVMSwapData_1 = require("../../evm/swaps/EVMSwapData");
13
13
  const EVMSpvVaultData_1 = require("../../evm/spv_swap/EVMSpvVaultData");
14
14
  const EVMSpvWithdrawalData_1 = require("../../evm/spv_swap/EVMSpvWithdrawalData");
15
+ const EVMChainEventsBrowserWS_1 = require("../../evm/events/EVMChainEventsBrowserWS");
15
16
  const BotanixChainIds = {
16
17
  MAINNET: null,
17
18
  TESTNET: 3636
@@ -77,7 +78,9 @@ function initializeBotanix(options, bitcoinRpc, network) {
77
78
  const defaultContractAddresses = BotanixContractAddresses[options.chainType];
78
79
  const chainId = BotanixChainIds[options.chainType];
79
80
  const provider = typeof (options.rpcUrl) === "string" ?
80
- new ethers_1.JsonRpcProvider(options.rpcUrl, { name: "Botanix", chainId }) :
81
+ (options.rpcUrl.startsWith("ws")
82
+ ? new ethers_1.WebSocketProvider(options.rpcUrl, { name: "Botanix", chainId })
83
+ : new ethers_1.JsonRpcProvider(options.rpcUrl, { name: "Botanix", chainId })) :
81
84
  options.rpcUrl;
82
85
  const Fees = options.fees ?? new EVMFees_1.EVMFees(provider, 2n * 1000000000n, 1000000n);
83
86
  const chainInterface = new EVMChainInterface_1.EVMChainInterface("BOTANIX", chainId, provider, {
@@ -96,7 +99,9 @@ function initializeBotanix(options, bitcoinRpc, network) {
96
99
  }
97
100
  });
98
101
  const spvVaultContract = new EVMSpvVaultContract_1.EVMSpvVaultContract(chainInterface, btcRelay, bitcoinRpc, options.spvVaultContract ?? defaultContractAddresses.spvVaultContract, options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight);
99
- const chainEvents = new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
102
+ const chainEvents = provider instanceof ethers_1.WebSocketProvider ?
103
+ new EVMChainEventsBrowserWS_1.EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
104
+ new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
100
105
  return {
101
106
  chainId: "BOTANIX",
102
107
  btcRelay,
@@ -10,4 +10,5 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import { CitreaSwapContract } from "./CitreaSwapContract";
11
11
  import { CitreaBtcRelay } from "./CitreaBtcRelay";
12
12
  import { CitreaSpvVaultContract } from "./CitreaSpvVaultContract";
13
- export type CitreaChainType = ChainType<"CITREA", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, CitreaSwapContract, EVMChainInterface<"CITREA", 5115>, EVMChainEventsBrowser, CitreaBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, CitreaSpvVaultContract>;
13
+ import { EVMChainEventsBrowserWS } from "../../evm/events/EVMChainEventsBrowserWS";
14
+ export type CitreaChainType = ChainType<"CITREA", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, CitreaSwapContract, EVMChainInterface<"CITREA", 5115>, EVMChainEventsBrowser | EVMChainEventsBrowserWS, CitreaBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, CitreaSpvVaultContract>;
@@ -13,6 +13,7 @@ const CitreaBtcRelay_1 = require("./CitreaBtcRelay");
13
13
  const CitreaSwapContract_1 = require("./CitreaSwapContract");
14
14
  const CitreaTokens_1 = require("./CitreaTokens");
15
15
  const CitreaSpvVaultContract_1 = require("./CitreaSpvVaultContract");
16
+ const EVMChainEventsBrowserWS_1 = require("../../evm/events/EVMChainEventsBrowserWS");
16
17
  const CitreaChainIds = {
17
18
  MAINNET: null,
18
19
  TESTNET4: 5115
@@ -83,7 +84,9 @@ function initializeCitrea(options, bitcoinRpc, network) {
83
84
  const defaultContractAddresses = CitreaContractAddresses[options.chainType];
84
85
  const chainId = CitreaChainIds[options.chainType];
85
86
  const provider = typeof (options.rpcUrl) === "string" ?
86
- new ethers_1.JsonRpcProvider(options.rpcUrl, { name: "Citrea", chainId }) :
87
+ (options.rpcUrl.startsWith("ws")
88
+ ? new ethers_1.WebSocketProvider(options.rpcUrl, { name: "Citrea", chainId })
89
+ : new ethers_1.JsonRpcProvider(options.rpcUrl, { name: "Citrea", chainId })) :
87
90
  options.rpcUrl;
88
91
  const Fees = options.fees ?? new CitreaFees_1.CitreaFees(provider, 2n * 1000000000n, 1000000n);
89
92
  const chainInterface = new EVMChainInterface_1.EVMChainInterface("CITREA", chainId, provider, {
@@ -103,7 +106,9 @@ function initializeCitrea(options, bitcoinRpc, network) {
103
106
  }
104
107
  });
105
108
  const spvVaultContract = new CitreaSpvVaultContract_1.CitreaSpvVaultContract(chainInterface, btcRelay, bitcoinRpc, options.spvVaultContract ?? defaultContractAddresses.spvVaultContract, options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight);
106
- const chainEvents = new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
109
+ const chainEvents = provider instanceof ethers_1.WebSocketProvider ?
110
+ new EVMChainEventsBrowserWS_1.EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
111
+ new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
107
112
  return {
108
113
  chainId: "CITREA",
109
114
  btcRelay,
@@ -1,4 +1,4 @@
1
- import { BaseContract } from "ethers";
1
+ import { BaseContract, Log } from "ethers";
2
2
  import { EVMEvents } from "../../chain/modules/EVMEvents";
3
3
  import { EVMContractBase } from "../EVMContractBase";
4
4
  import { EVMChainInterface } from "../../chain/EVMChainInterface";
@@ -7,7 +7,7 @@ export declare class EVMContractEvents<T extends BaseContract> extends EVMEvents
7
7
  readonly contract: EVMContractBase<T>;
8
8
  readonly baseContract: T;
9
9
  constructor(chainInterface: EVMChainInterface<any>, contract: EVMContractBase<T>);
10
- private toTypedEvents;
10
+ toTypedEvents<TEventName extends keyof T["filters"]>(blockEvents: Log[]): TypedEventLog<T["filters"][TEventName]>[];
11
11
  private toFilter;
12
12
  /**
13
13
  * Returns the events occuring in a range of EVM blocks as identified by the contract and keys,
@@ -0,0 +1,66 @@
1
+ import { ChainEvents, ClaimEvent, EventListener, InitializeEvent, RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent } from "@atomiqlabs/base";
2
+ import { IClaimHandler } from "../swaps/handlers/claim/ClaimHandlers";
3
+ import { EVMSwapData } from "../swaps/EVMSwapData";
4
+ import { Block, EventFilter, Log, WebSocketProvider } from "ethers";
5
+ import { EVMSwapContract } from "../swaps/EVMSwapContract";
6
+ import { EVMSpvVaultContract } from "../spv_swap/EVMSpvVaultContract";
7
+ import { EVMChainInterface } from "../chain/EVMChainInterface";
8
+ import { TypedEventLog } from "../typechain/common";
9
+ import { EscrowManager } from "../swaps/EscrowManagerTypechain";
10
+ import { SpvVaultManager } from "../spv_swap/SpvVaultContractTypechain";
11
+ import { EVMTxTrace } from "../chain/modules/EVMTransactions";
12
+ type AtomiqTypedEvent = (TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> | TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>);
13
+ /**
14
+ * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
15
+ * out on some events if the network is unreliable, front-end systems should take this into consideration and not
16
+ * rely purely on events
17
+ */
18
+ export declare class EVMChainEventsBrowserWS implements ChainEvents<EVMSwapData> {
19
+ protected readonly listeners: EventListener<EVMSwapData>[];
20
+ protected readonly provider: WebSocketProvider;
21
+ protected readonly chainInterface: EVMChainInterface;
22
+ protected readonly evmSwapContract: EVMSwapContract;
23
+ protected readonly evmSpvVaultContract: EVMSpvVaultContract<any>;
24
+ protected readonly logger: import("../../utils/Utils").LoggerType;
25
+ protected readonly spvVaultContractLogFilter: EventFilter;
26
+ protected readonly swapContractLogFilter: EventFilter;
27
+ protected unconfirmedEventQueue: AtomiqTypedEvent[];
28
+ protected stopped: boolean;
29
+ constructor(chainInterface: EVMChainInterface, evmSwapContract: EVMSwapContract, evmSpvVaultContract: EVMSpvVaultContract<any>);
30
+ findInitSwapData(call: EVMTxTrace, escrowHash: string, claimHandler: IClaimHandler<any, any>): EVMSwapData;
31
+ /**
32
+ * Returns async getter for fetching on-demand initialize event swap data
33
+ *
34
+ * @param event
35
+ * @param claimHandler
36
+ * @private
37
+ * @returns {() => Promise<EVMSwapData>} getter to be passed to InitializeEvent constructor
38
+ */
39
+ private getSwapDataGetter;
40
+ protected parseInitializeEvent(event: TypedEventLog<EscrowManager["filters"]["Initialize"]>): InitializeEvent<EVMSwapData>;
41
+ protected parseRefundEvent(event: TypedEventLog<EscrowManager["filters"]["Refund"]>): RefundEvent<EVMSwapData>;
42
+ protected parseClaimEvent(event: TypedEventLog<EscrowManager["filters"]["Claim"]>): ClaimEvent<EVMSwapData>;
43
+ protected parseSpvOpenEvent(event: TypedEventLog<SpvVaultManager["filters"]["Opened"]>): SpvVaultOpenEvent;
44
+ protected parseSpvDepositEvent(event: TypedEventLog<SpvVaultManager["filters"]["Deposited"]>): SpvVaultDepositEvent;
45
+ protected parseSpvFrontEvent(event: TypedEventLog<SpvVaultManager["filters"]["Fronted"]>): SpvVaultFrontEvent;
46
+ protected parseSpvClaimEvent(event: TypedEventLog<SpvVaultManager["filters"]["Claimed"]>): SpvVaultClaimEvent;
47
+ protected parseSpvCloseEvent(event: TypedEventLog<SpvVaultManager["filters"]["Closed"]>): SpvVaultCloseEvent;
48
+ /**
49
+ * Processes event as received from the chain, parses it & calls event listeners
50
+ *
51
+ * @param events
52
+ * @param currentBlock
53
+ * @protected
54
+ */
55
+ protected processEvents(events: AtomiqTypedEvent[], currentBlock?: Block): Promise<void>;
56
+ protected handleEvents(events: AtomiqTypedEvent[]): Promise<void>;
57
+ protected spvVaultContractListener: (log: Log) => void;
58
+ protected swapContractListener: (log: Log) => void;
59
+ protected blockListener: (blockNumber: number) => Promise<void>;
60
+ protected setupWebsocket(): Promise<void>;
61
+ init(): Promise<void>;
62
+ stop(): Promise<void>;
63
+ registerListener(cbk: EventListener<EVMSwapData>): void;
64
+ unregisterListener(cbk: EventListener<EVMSwapData>): boolean;
65
+ }
66
+ export {};
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EVMChainEventsBrowserWS = void 0;
4
+ const base_1 = require("@atomiqlabs/base");
5
+ const EVMSwapData_1 = require("../swaps/EVMSwapData");
6
+ const ethers_1 = require("ethers");
7
+ const Utils_1 = require("../../utils/Utils");
8
+ const EVMSpvVaultContract_1 = require("../spv_swap/EVMSpvVaultContract");
9
+ /**
10
+ * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
11
+ * out on some events if the network is unreliable, front-end systems should take this into consideration and not
12
+ * rely purely on events
13
+ */
14
+ class EVMChainEventsBrowserWS {
15
+ constructor(chainInterface, evmSwapContract, evmSpvVaultContract) {
16
+ this.listeners = [];
17
+ this.logger = (0, Utils_1.getLogger)("EVMChainEventsBrowser: ");
18
+ this.unconfirmedEventQueue = [];
19
+ this.chainInterface = chainInterface;
20
+ this.provider = chainInterface.provider;
21
+ this.evmSwapContract = evmSwapContract;
22
+ this.evmSpvVaultContract = evmSpvVaultContract;
23
+ this.spvVaultContractLogFilter = {
24
+ address: this.evmSpvVaultContract.contractAddress
25
+ };
26
+ this.swapContractLogFilter = {
27
+ address: this.evmSwapContract.contractAddress
28
+ };
29
+ }
30
+ findInitSwapData(call, escrowHash, claimHandler) {
31
+ if (call.to.toLowerCase() === this.evmSwapContract.contractAddress.toLowerCase()) {
32
+ const result = this.evmSwapContract.parseCalldata(call.input);
33
+ if (result != null && result.name === "initialize") {
34
+ //Found, check correct escrow hash
35
+ const [escrowData, signature, timeout, extraData] = result.args;
36
+ const escrow = EVMSwapData_1.EVMSwapData.deserializeFromStruct(escrowData, claimHandler);
37
+ if ("0x" + escrow.getEscrowHash() === escrowHash) {
38
+ const extraDataHex = (0, ethers_1.hexlify)(extraData);
39
+ if (extraDataHex.length > 2) {
40
+ escrow.setExtraData(extraDataHex.substring(2));
41
+ }
42
+ return escrow;
43
+ }
44
+ }
45
+ }
46
+ for (let _call of call.calls) {
47
+ const found = this.findInitSwapData(_call, escrowHash, claimHandler);
48
+ if (found != null)
49
+ return found;
50
+ }
51
+ return null;
52
+ }
53
+ /**
54
+ * Returns async getter for fetching on-demand initialize event swap data
55
+ *
56
+ * @param event
57
+ * @param claimHandler
58
+ * @private
59
+ * @returns {() => Promise<EVMSwapData>} getter to be passed to InitializeEvent constructor
60
+ */
61
+ getSwapDataGetter(event, claimHandler) {
62
+ return async () => {
63
+ const trace = await this.chainInterface.Transactions.traceTransaction(event.transactionHash);
64
+ if (trace == null)
65
+ return null;
66
+ return this.findInitSwapData(trace, event.args.escrowHash, claimHandler);
67
+ };
68
+ }
69
+ parseInitializeEvent(event) {
70
+ const escrowHash = event.args.escrowHash.substring(2);
71
+ const claimHandlerHex = event.args.claimHandler;
72
+ const claimHandler = this.evmSwapContract.claimHandlersByAddress[claimHandlerHex.toLowerCase()];
73
+ if (claimHandler == null) {
74
+ this.logger.warn("parseInitializeEvent(" + escrowHash + "): Unknown claim handler with claim: " + claimHandlerHex);
75
+ return null;
76
+ }
77
+ const swapType = claimHandler.getType();
78
+ this.logger.debug("InitializeEvent escrowHash: " + escrowHash);
79
+ return new base_1.InitializeEvent(escrowHash, swapType, (0, Utils_1.onceAsync)(this.getSwapDataGetter(event, claimHandler)));
80
+ }
81
+ parseRefundEvent(event) {
82
+ const escrowHash = event.args.escrowHash.substring(2);
83
+ this.logger.debug("RefundEvent escrowHash: " + escrowHash);
84
+ return new base_1.RefundEvent(escrowHash);
85
+ }
86
+ parseClaimEvent(event) {
87
+ const escrowHash = event.args.escrowHash.substring(2);
88
+ const claimHandlerHex = event.args.claimHandler;
89
+ const claimHandler = this.evmSwapContract.claimHandlersByAddress[claimHandlerHex.toLowerCase()];
90
+ if (claimHandler == null) {
91
+ this.logger.warn("parseClaimEvent(" + escrowHash + "): Unknown claim handler with claim: " + claimHandlerHex);
92
+ return null;
93
+ }
94
+ const witnessResult = event.args.witnessResult.substring(2);
95
+ this.logger.debug("ClaimEvent witnessResult: " + witnessResult + " escrowHash: " + escrowHash);
96
+ return new base_1.ClaimEvent(escrowHash, witnessResult);
97
+ }
98
+ parseSpvOpenEvent(event) {
99
+ const owner = event.args.owner;
100
+ const vaultId = event.args.vaultId;
101
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
102
+ const vout = Number(event.args.vout);
103
+ this.logger.debug("SpvOpenEvent owner: " + owner + " vaultId: " + vaultId + " utxo: " + btcTxId + ":" + vout);
104
+ return new base_1.SpvVaultOpenEvent(owner, vaultId, btcTxId, vout);
105
+ }
106
+ parseSpvDepositEvent(event) {
107
+ const [owner, vaultId] = (0, EVMSpvVaultContract_1.unpackOwnerAndVaultId)(event.args.ownerAndVaultId);
108
+ const amounts = [event.args.amount0, event.args.amount1];
109
+ const depositCount = Number(event.args.depositCount);
110
+ this.logger.debug("SpvDepositEvent owner: " + owner + " vaultId: " + vaultId + " depositCount: " + depositCount + " amounts: ", amounts);
111
+ return new base_1.SpvVaultDepositEvent(owner, vaultId, amounts, depositCount);
112
+ }
113
+ parseSpvFrontEvent(event) {
114
+ const [owner, vaultId] = (0, EVMSpvVaultContract_1.unpackOwnerAndVaultId)(event.args.ownerAndVaultId);
115
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
116
+ const recipient = event.args.recipient;
117
+ const executionHash = event.args.executionHash;
118
+ const amounts = [event.args.amount0, event.args.amount1];
119
+ const frontingAddress = event.args.caller;
120
+ this.logger.debug("SpvFrontEvent owner: " + owner + " vaultId: " + vaultId + " btcTxId: " + btcTxId +
121
+ " recipient: " + recipient + " frontedBy: " + frontingAddress + " amounts: ", amounts);
122
+ return new base_1.SpvVaultFrontEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, frontingAddress);
123
+ }
124
+ parseSpvClaimEvent(event) {
125
+ const [owner, vaultId] = (0, EVMSpvVaultContract_1.unpackOwnerAndVaultId)(event.args.ownerAndVaultId);
126
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
127
+ const recipient = event.args.recipient;
128
+ const executionHash = event.args.executionHash;
129
+ const amounts = [event.args.amount0, event.args.amount1];
130
+ const caller = event.args.caller;
131
+ const frontingAddress = event.args.frontingAddress;
132
+ const withdrawCount = Number(event.args.withdrawCount);
133
+ this.logger.debug("SpvClaimEvent owner: " + owner + " vaultId: " + vaultId + " btcTxId: " + btcTxId + " withdrawCount: " + withdrawCount +
134
+ " recipient: " + recipient + " frontedBy: " + frontingAddress + " claimedBy: " + caller + " amounts: ", amounts);
135
+ return new base_1.SpvVaultClaimEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, caller, frontingAddress, withdrawCount);
136
+ }
137
+ parseSpvCloseEvent(event) {
138
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
139
+ return new base_1.SpvVaultCloseEvent(event.args.owner, event.args.vaultId, btcTxId, event.args.error);
140
+ }
141
+ /**
142
+ * Processes event as received from the chain, parses it & calls event listeners
143
+ *
144
+ * @param events
145
+ * @param currentBlock
146
+ * @protected
147
+ */
148
+ async processEvents(events, currentBlock) {
149
+ const parsedEvents = [];
150
+ for (let event of events) {
151
+ let parsedEvent;
152
+ switch (event.eventName) {
153
+ case "Claim":
154
+ parsedEvent = this.parseClaimEvent(event);
155
+ break;
156
+ case "Refund":
157
+ parsedEvent = this.parseRefundEvent(event);
158
+ break;
159
+ case "Initialize":
160
+ parsedEvent = this.parseInitializeEvent(event);
161
+ break;
162
+ case "Opened":
163
+ parsedEvent = this.parseSpvOpenEvent(event);
164
+ break;
165
+ case "Deposited":
166
+ parsedEvent = this.parseSpvDepositEvent(event);
167
+ break;
168
+ case "Fronted":
169
+ parsedEvent = this.parseSpvFrontEvent(event);
170
+ break;
171
+ case "Claimed":
172
+ parsedEvent = this.parseSpvClaimEvent(event);
173
+ break;
174
+ case "Closed":
175
+ parsedEvent = this.parseSpvCloseEvent(event);
176
+ break;
177
+ }
178
+ const timestamp = event.blockNumber === currentBlock?.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
179
+ parsedEvent.meta = {
180
+ blockTime: timestamp,
181
+ txId: event.transactionHash,
182
+ timestamp //Maybe deprecated
183
+ };
184
+ parsedEvents.push(parsedEvent);
185
+ }
186
+ for (let listener of this.listeners) {
187
+ await listener(parsedEvents);
188
+ }
189
+ }
190
+ handleEvents(events) {
191
+ if (this.chainInterface.config.safeBlockTag === "latest" || this.chainInterface.config.safeBlockTag === "pending") {
192
+ return this.processEvents(events);
193
+ }
194
+ this.unconfirmedEventQueue.push(...events);
195
+ }
196
+ async setupWebsocket() {
197
+ await this.provider.on(this.spvVaultContractLogFilter, this.spvVaultContractListener = (log) => {
198
+ let events = this.evmSpvVaultContract.Events.toTypedEvents([log]);
199
+ events = events.filter(val => !val.removed);
200
+ this.handleEvents(events);
201
+ });
202
+ await this.provider.on(this.swapContractLogFilter, this.swapContractListener = (log) => {
203
+ let events = this.evmSwapContract.Events.toTypedEvents([log]);
204
+ events = events.filter(val => !val.removed && (val.eventName === "Initialize" || val.eventName === "Refund" || val.eventName === "Claim"));
205
+ this.handleEvents(events);
206
+ });
207
+ const safeBlockTag = this.chainInterface.config.safeBlockTag;
208
+ let processing = false;
209
+ if (safeBlockTag !== "latest" && safeBlockTag !== "pending")
210
+ await this.provider.on("block", this.blockListener = async (blockNumber) => {
211
+ if (processing)
212
+ return;
213
+ processing = true;
214
+ try {
215
+ const latestSafeBlock = await this.provider.getBlock(this.chainInterface.config.safeBlockTag);
216
+ const events = [];
217
+ this.unconfirmedEventQueue = this.unconfirmedEventQueue.filter(event => {
218
+ if (event.blockNumber <= latestSafeBlock.number) {
219
+ events.push(event);
220
+ return false;
221
+ }
222
+ return true;
223
+ });
224
+ const blocks = {};
225
+ for (let event of events) {
226
+ const block = blocks[event.blockNumber] ?? (blocks[event.blockNumber] = await this.provider.getBlock(event.blockNumber));
227
+ if (block.hash === event.blockHash) {
228
+ //Valid event
229
+ await this.processEvents([event], block);
230
+ }
231
+ else {
232
+ //Block hash doesn't match
233
+ }
234
+ }
235
+ }
236
+ catch (e) {
237
+ this.logger.error(`on('block'): Error when processing new block ${blockNumber}:`, e);
238
+ }
239
+ processing = false;
240
+ });
241
+ }
242
+ async init() {
243
+ await this.setupWebsocket();
244
+ this.stopped = false;
245
+ }
246
+ async stop() {
247
+ this.stopped = true;
248
+ await this.provider.off(this.spvVaultContractLogFilter, this.spvVaultContractListener);
249
+ await this.provider.off(this.swapContractLogFilter, this.swapContractListener);
250
+ await this.provider.off("block", this.blockListener);
251
+ }
252
+ registerListener(cbk) {
253
+ this.listeners.push(cbk);
254
+ }
255
+ unregisterListener(cbk) {
256
+ const index = this.listeners.indexOf(cbk);
257
+ if (index >= 0) {
258
+ this.listeners.splice(index, 1);
259
+ return true;
260
+ }
261
+ return false;
262
+ }
263
+ }
264
+ exports.EVMChainEventsBrowserWS = EVMChainEventsBrowserWS;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./evm/contract/modules/EVMContractEvents";
14
14
  export * from "./evm/contract/EVMContractBase";
15
15
  export * from "./evm/contract/EVMContractModule";
16
16
  export * from "./evm/events/EVMChainEventsBrowser";
17
+ export * from "./evm/events/EVMChainEventsBrowserWS";
17
18
  export * from "./evm/spv_swap/EVMSpvVaultContract";
18
19
  export * from "./evm/spv_swap/EVMSpvWithdrawalData";
19
20
  export * from "./evm/spv_swap/EVMSpvVaultData";
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ __exportStar(require("./evm/contract/modules/EVMContractEvents"), exports);
30
30
  __exportStar(require("./evm/contract/EVMContractBase"), exports);
31
31
  __exportStar(require("./evm/contract/EVMContractModule"), exports);
32
32
  __exportStar(require("./evm/events/EVMChainEventsBrowser"), exports);
33
+ __exportStar(require("./evm/events/EVMChainEventsBrowserWS"), exports);
33
34
  __exportStar(require("./evm/spv_swap/EVMSpvVaultContract"), exports);
34
35
  __exportStar(require("./evm/spv_swap/EVMSpvWithdrawalData"), exports);
35
36
  __exportStar(require("./evm/spv_swap/EVMSpvVaultData"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-evm",
3
- "version": "1.0.0-dev.48",
3
+ "version": "1.0.0-dev.49",
4
4
  "description": "EVM specific base implementation",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -10,6 +10,7 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import {EVMSwapContract} from "../../evm/swaps/EVMSwapContract";
11
11
  import {EVMBtcRelay} from "../../evm/btcrelay/EVMBtcRelay";
12
12
  import {EVMSpvVaultContract} from "../../evm/spv_swap/EVMSpvVaultContract";
13
+ import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
13
14
 
14
15
  export type BotanixChainType = ChainType<
15
16
  "BOTANIX",
@@ -20,7 +21,7 @@ export type BotanixChainType = ChainType<
20
21
  EVMSwapData,
21
22
  EVMSwapContract<"BOTANIX">,
22
23
  EVMChainInterface<"BOTANIX", 3636>,
23
- EVMChainEventsBrowser,
24
+ EVMChainEventsBrowser | EVMChainEventsBrowserWS,
24
25
  EVMBtcRelay<any>,
25
26
  EVMSpvVaultData,
26
27
  EVMSpvWithdrawalData,
@@ -1,5 +1,5 @@
1
1
  import {BaseTokenType, BitcoinNetwork, BitcoinRpc, ChainData, ChainInitializer, ChainSwapType} from "@atomiqlabs/base";
2
- import {JsonRpcApiProvider, JsonRpcProvider} from "ethers";
2
+ import {JsonRpcApiProvider, JsonRpcProvider, WebSocketProvider} from "ethers";
3
3
  import {EVMChainInterface, EVMRetryPolicy} from "../../evm/chain/EVMChainInterface";
4
4
  import {EVMFees} from "../../evm/chain/modules/EVMFees";
5
5
  import {EVMBtcRelay} from "../../evm/btcrelay/EVMBtcRelay";
@@ -10,6 +10,7 @@ import {EVMSwapData} from "../../evm/swaps/EVMSwapData";
10
10
  import {EVMSpvVaultData} from "../../evm/spv_swap/EVMSpvVaultData";
11
11
  import {EVMSpvWithdrawalData} from "../../evm/spv_swap/EVMSpvWithdrawalData";
12
12
  import {BotanixChainType} from "./BotanixChainType";
13
+ import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
13
14
 
14
15
  const BotanixChainIds = {
15
16
  MAINNET: null,
@@ -109,7 +110,11 @@ export function initializeBotanix(
109
110
  const chainId = BotanixChainIds[options.chainType];
110
111
 
111
112
  const provider = typeof(options.rpcUrl)==="string" ?
112
- new JsonRpcProvider(options.rpcUrl, {name: "Botanix", chainId}) :
113
+ (
114
+ options.rpcUrl.startsWith("ws")
115
+ ? new WebSocketProvider(options.rpcUrl, {name: "Botanix", chainId})
116
+ : new JsonRpcProvider(options.rpcUrl, {name: "Botanix", chainId})
117
+ ):
113
118
  options.rpcUrl;
114
119
 
115
120
  const Fees = options.fees ?? new EVMFees(provider, 2n * 1_000_000_000n, 1_000_000n);
@@ -142,7 +147,9 @@ export function initializeBotanix(
142
147
  options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight
143
148
  )
144
149
 
145
- const chainEvents = new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
150
+ const chainEvents = provider instanceof WebSocketProvider ?
151
+ new EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
152
+ new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
146
153
 
147
154
  return {
148
155
  chainId: "BOTANIX",
@@ -10,6 +10,7 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import {CitreaSwapContract} from "./CitreaSwapContract";
11
11
  import {CitreaBtcRelay} from "./CitreaBtcRelay";
12
12
  import {CitreaSpvVaultContract} from "./CitreaSpvVaultContract";
13
+ import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
13
14
 
14
15
  export type CitreaChainType = ChainType<
15
16
  "CITREA",
@@ -20,7 +21,7 @@ export type CitreaChainType = ChainType<
20
21
  EVMSwapData,
21
22
  CitreaSwapContract,
22
23
  EVMChainInterface<"CITREA", 5115>,
23
- EVMChainEventsBrowser,
24
+ EVMChainEventsBrowser | EVMChainEventsBrowserWS,
24
25
  CitreaBtcRelay<any>,
25
26
  EVMSpvVaultData,
26
27
  EVMSpvWithdrawalData,
@@ -1,11 +1,7 @@
1
1
  import {BaseTokenType, BitcoinNetwork, BitcoinRpc, ChainData, ChainInitializer, ChainSwapType} from "@atomiqlabs/base";
2
- import {JsonRpcApiProvider, JsonRpcProvider} from "ethers";
2
+ import {JsonRpcApiProvider, JsonRpcProvider, WebSocketProvider} from "ethers";
3
3
  import {EVMChainInterface, EVMRetryPolicy} from "../../evm/chain/EVMChainInterface";
4
- import {EVMFees} from "../../evm/chain/modules/EVMFees";
5
4
  import {CitreaChainType} from "./CitreaChainType";
6
- import {EVMBtcRelay} from "../../evm/btcrelay/EVMBtcRelay";
7
- import {EVMSwapContract} from "../../evm/swaps/EVMSwapContract";
8
- import {EVMSpvVaultContract} from "../../evm/spv_swap/EVMSpvVaultContract";
9
5
  import {EVMChainEventsBrowser} from "../../evm/events/EVMChainEventsBrowser";
10
6
  import {EVMSwapData} from "../../evm/swaps/EVMSwapData";
11
7
  import {EVMSpvVaultData} from "../../evm/spv_swap/EVMSpvVaultData";
@@ -15,6 +11,7 @@ import {CitreaBtcRelay} from "./CitreaBtcRelay";
15
11
  import {CitreaSwapContract} from "./CitreaSwapContract";
16
12
  import {CitreaTokens} from "./CitreaTokens";
17
13
  import {CitreaSpvVaultContract} from "./CitreaSpvVaultContract";
14
+ import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
18
15
 
19
16
  const CitreaChainIds = {
20
17
  MAINNET: null,
@@ -119,7 +116,11 @@ export function initializeCitrea(
119
116
  const chainId = CitreaChainIds[options.chainType];
120
117
 
121
118
  const provider = typeof(options.rpcUrl)==="string" ?
122
- new JsonRpcProvider(options.rpcUrl, {name: "Citrea", chainId}) :
119
+ (
120
+ options.rpcUrl.startsWith("ws")
121
+ ? new WebSocketProvider(options.rpcUrl, {name: "Citrea", chainId})
122
+ : new JsonRpcProvider(options.rpcUrl, {name: "Citrea", chainId})
123
+ ):
123
124
  options.rpcUrl;
124
125
 
125
126
  const Fees = options.fees ?? new CitreaFees(provider, 2n * 1_000_000_000n, 1_000_000n);
@@ -153,7 +154,9 @@ export function initializeCitrea(
153
154
  options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight
154
155
  )
155
156
 
156
- const chainEvents = new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
157
+ const chainEvents = provider instanceof WebSocketProvider ?
158
+ new EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
159
+ new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
157
160
 
158
161
  return {
159
162
  chainId: "CITREA",
@@ -15,7 +15,7 @@ export class EVMContractEvents<T extends BaseContract> extends EVMEvents {
15
15
  this.baseContract = contract.contract;
16
16
  }
17
17
 
18
- private toTypedEvents<TEventName extends keyof T["filters"]>(blockEvents: Log[]): TypedEventLog<T["filters"][TEventName]>[] {
18
+ public toTypedEvents<TEventName extends keyof T["filters"]>(blockEvents: Log[]): TypedEventLog<T["filters"][TEventName]>[] {
19
19
  return blockEvents.map(log => this.contract.toTypedEvent<TEventName>(log));
20
20
  }
21
21
 
@@ -0,0 +1,354 @@
1
+ import {
2
+ ChainEvent,
3
+ ChainEvents,
4
+ ChainSwapType,
5
+ ClaimEvent,
6
+ EventListener,
7
+ InitializeEvent,
8
+ RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent
9
+ } from "@atomiqlabs/base";
10
+ import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";
11
+ import {EVMSwapData} from "../swaps/EVMSwapData";
12
+ import {Block, EventFilter, hexlify, Log, WebSocketProvider} from "ethers";
13
+ import { EVMSwapContract } from "../swaps/EVMSwapContract";
14
+ import {getLogger, onceAsync} from "../../utils/Utils";
15
+ import {EVMSpvVaultContract, unpackOwnerAndVaultId} from "../spv_swap/EVMSpvVaultContract";
16
+ import { EVMChainInterface } from "../chain/EVMChainInterface";
17
+ import {TypedEventLog} from "../typechain/common";
18
+ import {EscrowManager} from "../swaps/EscrowManagerTypechain";
19
+ import {SpvVaultManager} from "../spv_swap/SpvVaultContractTypechain";
20
+ import {EVMTxTrace} from "../chain/modules/EVMTransactions";
21
+
22
+ type AtomiqTypedEvent = (
23
+ TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> |
24
+ TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>
25
+ );
26
+
27
+ /**
28
+ * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
29
+ * out on some events if the network is unreliable, front-end systems should take this into consideration and not
30
+ * rely purely on events
31
+ */
32
+ export class EVMChainEventsBrowserWS implements ChainEvents<EVMSwapData> {
33
+
34
+ protected readonly listeners: EventListener<EVMSwapData>[] = [];
35
+ protected readonly provider: WebSocketProvider;
36
+ protected readonly chainInterface: EVMChainInterface;
37
+ protected readonly evmSwapContract: EVMSwapContract;
38
+ protected readonly evmSpvVaultContract: EVMSpvVaultContract<any>;
39
+ protected readonly logger = getLogger("EVMChainEventsBrowser: ");
40
+
41
+ protected readonly spvVaultContractLogFilter: EventFilter;
42
+ protected readonly swapContractLogFilter: EventFilter;
43
+
44
+ protected unconfirmedEventQueue: AtomiqTypedEvent[] = [];
45
+
46
+ protected stopped: boolean;
47
+
48
+ constructor(
49
+ chainInterface: EVMChainInterface,
50
+ evmSwapContract: EVMSwapContract,
51
+ evmSpvVaultContract: EVMSpvVaultContract<any>
52
+ ) {
53
+ this.chainInterface = chainInterface;
54
+ this.provider = chainInterface.provider as WebSocketProvider;
55
+ this.evmSwapContract = evmSwapContract;
56
+ this.evmSpvVaultContract = evmSpvVaultContract;
57
+
58
+ this.spvVaultContractLogFilter = {
59
+ address: this.evmSpvVaultContract.contractAddress
60
+ };
61
+ this.swapContractLogFilter = {
62
+ address: this.evmSwapContract.contractAddress
63
+ };
64
+ }
65
+
66
+ findInitSwapData(call: EVMTxTrace, escrowHash: string, claimHandler: IClaimHandler<any, any>): EVMSwapData {
67
+ if(call.to.toLowerCase() === this.evmSwapContract.contractAddress.toLowerCase()) {
68
+ const result = this.evmSwapContract.parseCalldata<typeof this.evmSwapContract.contract.initialize>(call.input);
69
+ if(result!=null && result.name==="initialize") {
70
+ //Found, check correct escrow hash
71
+ const [escrowData, signature, timeout, extraData] = result.args;
72
+ const escrow = EVMSwapData.deserializeFromStruct(escrowData, claimHandler);
73
+ if("0x"+escrow.getEscrowHash()===escrowHash) {
74
+ const extraDataHex = hexlify(extraData);
75
+ if(extraDataHex.length>2) {
76
+ escrow.setExtraData(extraDataHex.substring(2));
77
+ }
78
+ return escrow;
79
+ }
80
+ }
81
+ }
82
+ for(let _call of call.calls) {
83
+ const found = this.findInitSwapData(_call, escrowHash, claimHandler);
84
+ if(found!=null) return found;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Returns async getter for fetching on-demand initialize event swap data
91
+ *
92
+ * @param event
93
+ * @param claimHandler
94
+ * @private
95
+ * @returns {() => Promise<EVMSwapData>} getter to be passed to InitializeEvent constructor
96
+ */
97
+ private getSwapDataGetter(
98
+ event: TypedEventLog<EscrowManager["filters"]["Initialize"]>,
99
+ claimHandler: IClaimHandler<any, any>
100
+ ): () => Promise<EVMSwapData> {
101
+ return async () => {
102
+ const trace = await this.chainInterface.Transactions.traceTransaction(event.transactionHash);
103
+ if(trace==null) return null;
104
+ return this.findInitSwapData(trace, event.args.escrowHash, claimHandler);
105
+ }
106
+ }
107
+
108
+ protected parseInitializeEvent(
109
+ event: TypedEventLog<EscrowManager["filters"]["Initialize"]>
110
+ ): InitializeEvent<EVMSwapData> {
111
+ const escrowHash = event.args.escrowHash.substring(2);
112
+ const claimHandlerHex = event.args.claimHandler;
113
+ const claimHandler = this.evmSwapContract.claimHandlersByAddress[claimHandlerHex.toLowerCase()];
114
+ if(claimHandler==null) {
115
+ this.logger.warn("parseInitializeEvent("+escrowHash+"): Unknown claim handler with claim: "+claimHandlerHex);
116
+ return null;
117
+ }
118
+ const swapType: ChainSwapType = claimHandler.getType();
119
+
120
+ this.logger.debug("InitializeEvent escrowHash: "+escrowHash);
121
+ return new InitializeEvent<EVMSwapData>(
122
+ escrowHash,
123
+ swapType,
124
+ onceAsync<EVMSwapData>(this.getSwapDataGetter(event, claimHandler))
125
+ );
126
+ }
127
+
128
+ protected parseRefundEvent(
129
+ event: TypedEventLog<EscrowManager["filters"]["Refund"]>
130
+ ): RefundEvent<EVMSwapData> {
131
+ const escrowHash = event.args.escrowHash.substring(2);
132
+ this.logger.debug("RefundEvent escrowHash: "+escrowHash);
133
+ return new RefundEvent<EVMSwapData>(escrowHash);
134
+ }
135
+
136
+ protected parseClaimEvent(
137
+ event: TypedEventLog<EscrowManager["filters"]["Claim"]>
138
+ ): ClaimEvent<EVMSwapData> {
139
+ const escrowHash = event.args.escrowHash.substring(2);
140
+ const claimHandlerHex = event.args.claimHandler;
141
+ const claimHandler = this.evmSwapContract.claimHandlersByAddress[claimHandlerHex.toLowerCase()];
142
+ if(claimHandler==null) {
143
+ this.logger.warn("parseClaimEvent("+escrowHash+"): Unknown claim handler with claim: "+claimHandlerHex);
144
+ return null;
145
+ }
146
+ const witnessResult = event.args.witnessResult.substring(2);
147
+ this.logger.debug("ClaimEvent witnessResult: "+witnessResult+" escrowHash: "+escrowHash);
148
+ return new ClaimEvent<EVMSwapData>(escrowHash, witnessResult);
149
+ }
150
+
151
+ protected parseSpvOpenEvent(
152
+ event: TypedEventLog<SpvVaultManager["filters"]["Opened"]>
153
+ ): SpvVaultOpenEvent {
154
+ const owner = event.args.owner;
155
+ const vaultId = event.args.vaultId;
156
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
157
+ const vout = Number(event.args.vout);
158
+
159
+ this.logger.debug("SpvOpenEvent owner: "+owner+" vaultId: "+vaultId+" utxo: "+btcTxId+":"+vout);
160
+ return new SpvVaultOpenEvent(owner, vaultId, btcTxId, vout);
161
+ }
162
+
163
+ protected parseSpvDepositEvent(
164
+ event: TypedEventLog<SpvVaultManager["filters"]["Deposited"]>
165
+ ): SpvVaultDepositEvent {
166
+ const [owner, vaultId] = unpackOwnerAndVaultId(event.args.ownerAndVaultId);
167
+ const amounts = [event.args.amount0, event.args.amount1];
168
+ const depositCount = Number(event.args.depositCount);
169
+
170
+ this.logger.debug("SpvDepositEvent owner: "+owner+" vaultId: "+vaultId+" depositCount: "+depositCount+" amounts: ", amounts);
171
+ return new SpvVaultDepositEvent(owner, vaultId, amounts, depositCount);
172
+ }
173
+
174
+ protected parseSpvFrontEvent(
175
+ event: TypedEventLog<SpvVaultManager["filters"]["Fronted"]>
176
+ ): SpvVaultFrontEvent {
177
+ const [owner, vaultId] = unpackOwnerAndVaultId(event.args.ownerAndVaultId);
178
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
179
+ const recipient = event.args.recipient;
180
+ const executionHash = event.args.executionHash;
181
+ const amounts = [event.args.amount0, event.args.amount1];
182
+ const frontingAddress = event.args.caller;
183
+
184
+ this.logger.debug("SpvFrontEvent owner: "+owner+" vaultId: "+vaultId+" btcTxId: "+btcTxId+
185
+ " recipient: "+recipient+" frontedBy: "+frontingAddress+" amounts: ", amounts);
186
+ return new SpvVaultFrontEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, frontingAddress);
187
+ }
188
+
189
+ protected parseSpvClaimEvent(
190
+ event: TypedEventLog<SpvVaultManager["filters"]["Claimed"]>
191
+ ): SpvVaultClaimEvent {
192
+ const [owner, vaultId] = unpackOwnerAndVaultId(event.args.ownerAndVaultId);
193
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
194
+ const recipient = event.args.recipient;
195
+ const executionHash = event.args.executionHash;
196
+ const amounts = [event.args.amount0, event.args.amount1];
197
+ const caller = event.args.caller;
198
+ const frontingAddress = event.args.frontingAddress;
199
+ const withdrawCount = Number(event.args.withdrawCount);
200
+
201
+ this.logger.debug("SpvClaimEvent owner: "+owner+" vaultId: "+vaultId+" btcTxId: "+btcTxId+" withdrawCount: "+withdrawCount+
202
+ " recipient: "+recipient+" frontedBy: "+frontingAddress+" claimedBy: "+caller+" amounts: ", amounts);
203
+
204
+ return new SpvVaultClaimEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, caller, frontingAddress, withdrawCount);
205
+ }
206
+
207
+ protected parseSpvCloseEvent(
208
+ event: TypedEventLog<SpvVaultManager["filters"]["Closed"]>
209
+ ): SpvVaultCloseEvent {
210
+ const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
211
+
212
+ return new SpvVaultCloseEvent(event.args.owner, event.args.vaultId, btcTxId, event.args.error);
213
+ }
214
+
215
+ /**
216
+ * Processes event as received from the chain, parses it & calls event listeners
217
+ *
218
+ * @param events
219
+ * @param currentBlock
220
+ * @protected
221
+ */
222
+ protected async processEvents(
223
+ events: AtomiqTypedEvent[],
224
+ currentBlock?: Block
225
+ ) {
226
+ const parsedEvents: ChainEvent<EVMSwapData>[] = [];
227
+
228
+ for(let event of events) {
229
+ let parsedEvent: ChainEvent<EVMSwapData>;
230
+ switch(event.eventName) {
231
+ case "Claim":
232
+ parsedEvent = this.parseClaimEvent(event as any);
233
+ break;
234
+ case "Refund":
235
+ parsedEvent = this.parseRefundEvent(event as any);
236
+ break;
237
+ case "Initialize":
238
+ parsedEvent = this.parseInitializeEvent(event as any);
239
+ break;
240
+ case "Opened":
241
+ parsedEvent = this.parseSpvOpenEvent(event as any);
242
+ break;
243
+ case "Deposited":
244
+ parsedEvent = this.parseSpvDepositEvent(event as any);
245
+ break;
246
+ case "Fronted":
247
+ parsedEvent = this.parseSpvFrontEvent(event as any);
248
+ break;
249
+ case "Claimed":
250
+ parsedEvent = this.parseSpvClaimEvent(event as any);
251
+ break;
252
+ case "Closed":
253
+ parsedEvent = this.parseSpvCloseEvent(event as any);
254
+ break;
255
+ }
256
+ const timestamp = event.blockNumber===currentBlock?.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
257
+ parsedEvent.meta = {
258
+ blockTime: timestamp,
259
+ txId: event.transactionHash,
260
+ timestamp //Maybe deprecated
261
+ } as any;
262
+ parsedEvents.push(parsedEvent);
263
+ }
264
+
265
+ for(let listener of this.listeners) {
266
+ await listener(parsedEvents);
267
+ }
268
+ }
269
+
270
+ protected handleEvents(
271
+ events : AtomiqTypedEvent[]
272
+ ): Promise<void> {
273
+ if(this.chainInterface.config.safeBlockTag==="latest" || this.chainInterface.config.safeBlockTag==="pending") {
274
+ return this.processEvents(events);
275
+ }
276
+ this.unconfirmedEventQueue.push(...events);
277
+ }
278
+
279
+ protected spvVaultContractListener: (log: Log) => void;
280
+ protected swapContractListener: (log: Log) => void;
281
+ protected blockListener: (blockNumber: number) => Promise<void>;
282
+
283
+ protected async setupWebsocket() {
284
+ await this.provider.on(this.spvVaultContractLogFilter, this.spvVaultContractListener = (log) => {
285
+ let events = this.evmSpvVaultContract.Events.toTypedEvents([log]);
286
+ events = events.filter(val => !val.removed);
287
+ this.handleEvents(events);
288
+ });
289
+
290
+ await this.provider.on(this.swapContractLogFilter, this.swapContractListener = (log) => {
291
+ let events = this.evmSwapContract.Events.toTypedEvents([log]);
292
+ events = events.filter(val => !val.removed && (val.eventName==="Initialize" || val.eventName==="Refund" || val.eventName==="Claim"));
293
+ this.handleEvents(events);
294
+ });
295
+
296
+ const safeBlockTag = this.chainInterface.config.safeBlockTag;
297
+ let processing = false;
298
+ if(safeBlockTag!=="latest" && safeBlockTag!=="pending") await this.provider.on("block", this.blockListener = async (blockNumber: number) => {
299
+ if(processing) return;
300
+ processing = true;
301
+ try {
302
+ const latestSafeBlock = await this.provider.getBlock(this.chainInterface.config.safeBlockTag);
303
+
304
+ const events = [];
305
+ this.unconfirmedEventQueue = this.unconfirmedEventQueue.filter(event => {
306
+ if(event.blockNumber <= latestSafeBlock.number) {
307
+ events.push(event);
308
+ return false;
309
+ }
310
+ return true;
311
+ });
312
+
313
+ const blocks: {[blockNumber: number]: Block} = {};
314
+ for(let event of events) {
315
+ const block = blocks[event.blockNumber] ?? (blocks[event.blockNumber] = await this.provider.getBlock(event.blockNumber));
316
+ if(block.hash===event.blockHash) {
317
+ //Valid event
318
+ await this.processEvents([event], block);
319
+ } else {
320
+ //Block hash doesn't match
321
+ }
322
+ }
323
+ } catch (e) {
324
+ this.logger.error(`on('block'): Error when processing new block ${blockNumber}:`, e);
325
+ }
326
+ processing = false;
327
+ });
328
+ }
329
+
330
+ async init(): Promise<void> {
331
+ await this.setupWebsocket();
332
+ this.stopped = false;
333
+ }
334
+
335
+ async stop(): Promise<void> {
336
+ this.stopped = true;
337
+ await this.provider.off(this.spvVaultContractLogFilter, this.spvVaultContractListener);
338
+ await this.provider.off(this.swapContractLogFilter, this.swapContractListener);
339
+ await this.provider.off("block", this.blockListener);
340
+ }
341
+
342
+ registerListener(cbk: EventListener<EVMSwapData>): void {
343
+ this.listeners.push(cbk);
344
+ }
345
+
346
+ unregisterListener(cbk: EventListener<EVMSwapData>): boolean {
347
+ const index = this.listeners.indexOf(cbk);
348
+ if(index>=0) {
349
+ this.listeners.splice(index, 1);
350
+ return true;
351
+ }
352
+ return false;
353
+ }
354
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export * from "./evm/contract/EVMContractBase";
17
17
  export * from "./evm/contract/EVMContractModule";
18
18
 
19
19
  export * from "./evm/events/EVMChainEventsBrowser";
20
+ export * from "./evm/events/EVMChainEventsBrowserWS";
20
21
 
21
22
  export * from "./evm/spv_swap/EVMSpvVaultContract";
22
23
  export * from "./evm/spv_swap/EVMSpvWithdrawalData";