@atomiqlabs/chain-starknet 7.0.3 → 7.0.5
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.
- package/dist/starknet/StarknetInitializer.d.ts +2 -1
- package/dist/starknet/StarknetInitializer.js +6 -1
- package/dist/starknet/chain/StarknetChainInterface.d.ts +3 -2
- package/dist/starknet/chain/StarknetChainInterface.js +2 -1
- package/dist/starknet/contract/modules/StarknetContractEvents.d.ts +3 -3
- package/dist/starknet/events/StarknetChainEvents.js +19 -18
- package/dist/starknet/events/StarknetChainEventsBrowser.d.ts +23 -11
- package/dist/starknet/events/StarknetChainEventsBrowser.js +143 -43
- package/package.json +3 -2
- package/src/starknet/StarknetInitializer.ts +7 -2
- package/src/starknet/chain/StarknetChainInterface.ts +4 -1
- package/src/starknet/contract/modules/StarknetContractEvents.ts +2 -2
- package/src/starknet/events/StarknetChainEvents.ts +22 -21
- package/src/starknet/events/StarknetChainEventsBrowser.ts +185 -49
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { constants, Provider } from "starknet";
|
|
1
|
+
import { constants, Provider, WebSocketChannel } from "starknet";
|
|
2
2
|
import { StarknetFees } from "./chain/modules/StarknetFees";
|
|
3
3
|
import { StarknetConfig, StarknetRetryPolicy } from "./chain/StarknetChainInterface";
|
|
4
4
|
import { BaseTokenType, BitcoinNetwork, BitcoinRpc, ChainData, ChainInitializer, ChainSwapType } from "@atomiqlabs/base";
|
|
@@ -7,6 +7,7 @@ export type StarknetAssetsType = BaseTokenType<"ETH" | "STRK" | "WBTC" | "TBTC"
|
|
|
7
7
|
export declare const StarknetAssets: StarknetAssetsType;
|
|
8
8
|
export type StarknetOptions = {
|
|
9
9
|
rpcUrl: string | Provider;
|
|
10
|
+
wsUrl?: string | WebSocketChannel;
|
|
10
11
|
retryPolicy?: StarknetRetryPolicy;
|
|
11
12
|
chainId?: constants.StarknetChainId;
|
|
12
13
|
swapContract?: string;
|
|
@@ -42,10 +42,15 @@ function initializeStarknet(options, bitcoinRpc, network) {
|
|
|
42
42
|
const provider = typeof (options.rpcUrl) === "string" ?
|
|
43
43
|
new RpcProviderWithRetries_1.RpcProviderWithRetries({ nodeUrl: options.rpcUrl }) :
|
|
44
44
|
options.rpcUrl;
|
|
45
|
+
let wsChannel;
|
|
46
|
+
if (options.wsUrl != null)
|
|
47
|
+
wsChannel = typeof (options.wsUrl) === "string" ?
|
|
48
|
+
new starknet_1.WebSocketChannel({ nodeUrl: options.wsUrl, websocket: typeof window !== "undefined" && typeof window.WebSocket !== "undefined" ? window.WebSocket : require("ws") }) :
|
|
49
|
+
options.wsUrl;
|
|
45
50
|
const Fees = options.fees ?? new StarknetFees_1.StarknetFees(provider);
|
|
46
51
|
const chainId = options.chainId ??
|
|
47
52
|
(network === base_1.BitcoinNetwork.MAINNET ? starknet_1.constants.StarknetChainId.SN_MAIN : starknet_1.constants.StarknetChainId.SN_SEPOLIA);
|
|
48
|
-
const chainInterface = new StarknetChainInterface_1.StarknetChainInterface(chainId, provider, options.retryPolicy, Fees, options.starknetConfig);
|
|
53
|
+
const chainInterface = new StarknetChainInterface_1.StarknetChainInterface(chainId, provider, wsChannel, options.retryPolicy, Fees, options.starknetConfig);
|
|
49
54
|
const btcRelay = new StarknetBtcRelay_1.StarknetBtcRelay(chainInterface, bitcoinRpc, network, options.btcRelayContract);
|
|
50
55
|
const swapContract = new StarknetSwapContract_1.StarknetSwapContract(chainInterface, btcRelay, options.swapContract, options.handlerContracts);
|
|
51
56
|
const spvVaultContract = new StarknetSpvVaultContract_1.StarknetSpvVaultContract(chainInterface, btcRelay, bitcoinRpc, options.spvVaultContract);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Provider, constants, Account } from "starknet";
|
|
1
|
+
import { Provider, constants, Account, WebSocketChannel } from "starknet";
|
|
2
2
|
import { StarknetTransactions, StarknetTx } from "./modules/StarknetTransactions";
|
|
3
3
|
import { StarknetFees } from "./modules/StarknetFees";
|
|
4
4
|
import { StarknetTokens } from "./modules/StarknetTokens";
|
|
@@ -21,6 +21,7 @@ export type StarknetConfig = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare class StarknetChainInterface implements ChainInterface<StarknetTx, StarknetSigner, "STARKNET", Account> {
|
|
23
23
|
readonly chainId = "STARKNET";
|
|
24
|
+
readonly wsChannel?: WebSocketChannel;
|
|
24
25
|
readonly provider: Provider;
|
|
25
26
|
readonly retryPolicy: StarknetRetryPolicy;
|
|
26
27
|
readonly starknetChainId: constants.StarknetChainId;
|
|
@@ -33,7 +34,7 @@ export declare class StarknetChainInterface implements ChainInterface<StarknetTx
|
|
|
33
34
|
readonly Blocks: StarknetBlocks;
|
|
34
35
|
protected readonly logger: import("../../utils/Utils").LoggerType;
|
|
35
36
|
readonly config: StarknetConfig;
|
|
36
|
-
constructor(chainId: constants.StarknetChainId, provider: Provider, retryPolicy?: StarknetRetryPolicy, feeEstimator?: StarknetFees, options?: StarknetConfig);
|
|
37
|
+
constructor(chainId: constants.StarknetChainId, provider: Provider, wsChannel?: WebSocketChannel, retryPolicy?: StarknetRetryPolicy, feeEstimator?: StarknetFees, options?: StarknetConfig);
|
|
37
38
|
getBalance(signer: string, tokenAddress: string): Promise<bigint>;
|
|
38
39
|
getNativeCurrencyAddress(): string;
|
|
39
40
|
isValidToken(tokenIdentifier: string): boolean;
|
|
@@ -16,7 +16,7 @@ const buffer_1 = require("buffer");
|
|
|
16
16
|
const StarknetKeypairWallet_1 = require("../wallet/accounts/StarknetKeypairWallet");
|
|
17
17
|
const StarknetBrowserSigner_1 = require("../wallet/StarknetBrowserSigner");
|
|
18
18
|
class StarknetChainInterface {
|
|
19
|
-
constructor(chainId, provider, retryPolicy, feeEstimator = new StarknetFees_1.StarknetFees(provider), options) {
|
|
19
|
+
constructor(chainId, provider, wsChannel, retryPolicy, feeEstimator = new StarknetFees_1.StarknetFees(provider), options) {
|
|
20
20
|
var _a, _b, _c, _d;
|
|
21
21
|
this.chainId = "STARKNET";
|
|
22
22
|
this.logger = (0, Utils_1.getLogger)("StarknetChainInterface: ");
|
|
@@ -28,6 +28,7 @@ class StarknetChainInterface {
|
|
|
28
28
|
(_b = this.config).getLogChunkSize ?? (_b.getLogChunkSize = 100);
|
|
29
29
|
(_c = this.config).maxGetLogKeys ?? (_c.maxGetLogKeys = 64);
|
|
30
30
|
(_d = this.config).maxParallelCalls ?? (_d.maxParallelCalls = 10);
|
|
31
|
+
this.wsChannel = wsChannel;
|
|
31
32
|
this.Fees = feeEstimator;
|
|
32
33
|
this.Tokens = new StarknetTokens_1.StarknetTokens(this);
|
|
33
34
|
this.Transactions = new StarknetTransactions_1.StarknetTransactions(this);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Abi } from "abi-wan-kanabi";
|
|
2
2
|
import { EventToPrimitiveType, ExtractAbiEventNames } from "abi-wan-kanabi/dist/kanabi";
|
|
3
|
-
import { StarknetEvents } from "../../chain/modules/StarknetEvents";
|
|
3
|
+
import { StarknetEvent, StarknetEvents } from "../../chain/modules/StarknetEvents";
|
|
4
4
|
import { StarknetContractBase } from "../StarknetContractBase";
|
|
5
5
|
import { StarknetChainInterface } from "../../chain/StarknetChainInterface";
|
|
6
6
|
export type StarknetAbiEvent<TAbi extends Abi, TEventName extends ExtractAbiEventNames<TAbi>> = {
|
|
@@ -16,8 +16,8 @@ export declare class StarknetContractEvents<TAbi extends Abi> extends StarknetEv
|
|
|
16
16
|
readonly contract: StarknetContractBase<TAbi>;
|
|
17
17
|
readonly abi: TAbi;
|
|
18
18
|
constructor(chainInterface: StarknetChainInterface, contract: StarknetContractBase<TAbi>, abi: TAbi);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
toStarknetAbiEvents<T extends ExtractAbiEventNames<TAbi>>(blockEvents: StarknetEvent[]): StarknetAbiEvent<TAbi, T>[];
|
|
20
|
+
toFilter<T extends ExtractAbiEventNames<TAbi>>(events: T[], keys: (string | string[])[]): string[][];
|
|
21
21
|
/**
|
|
22
22
|
* Returns the events occuring in a range of starknet block as identified by the contract and keys,
|
|
23
23
|
* returns pending events if no startHeight & endHeight is passed
|
|
@@ -18,22 +18,21 @@ class StarknetChainEvents extends StarknetChainEventsBrowser_1.StarknetChainEven
|
|
|
18
18
|
async getLastEventData() {
|
|
19
19
|
try {
|
|
20
20
|
const txt = (await fs.readFile(this.directory + BLOCKHEIGHT_FILENAME)).toString();
|
|
21
|
-
const arr = txt.split("
|
|
22
|
-
if (arr.length < 2)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
const arr = txt.split(",");
|
|
22
|
+
if (arr.length < 2) {
|
|
23
|
+
const blockNumber = parseInt(arr[0].split(";")[0]);
|
|
24
|
+
return [
|
|
25
|
+
{ lastBlockNumber: blockNumber, lastTxHash: null },
|
|
26
|
+
{ lastBlockNumber: blockNumber, lastTxHash: null }
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
return arr.map(arrValue => {
|
|
30
|
+
const subArray = arrValue.split(";");
|
|
31
|
+
return { lastBlockNumber: parseInt(subArray[0]), lastTxHash: subArray[1] };
|
|
32
|
+
});
|
|
31
33
|
}
|
|
32
34
|
catch (e) {
|
|
33
|
-
return
|
|
34
|
-
blockNumber: null,
|
|
35
|
-
txHashes: null
|
|
36
|
-
};
|
|
35
|
+
return [];
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
/**
|
|
@@ -41,12 +40,14 @@ class StarknetChainEvents extends StarknetChainEventsBrowser_1.StarknetChainEven
|
|
|
41
40
|
*
|
|
42
41
|
* @private
|
|
43
42
|
*/
|
|
44
|
-
saveLastEventData(
|
|
45
|
-
return fs.writeFile(this.directory + BLOCKHEIGHT_FILENAME,
|
|
43
|
+
saveLastEventData(newState) {
|
|
44
|
+
return fs.writeFile(this.directory + BLOCKHEIGHT_FILENAME, newState.map(value => value.lastTxHash == null ? value.lastBlockNumber.toString(10) : value.lastBlockNumber.toString(10) + ";" + value.lastTxHash).join(","));
|
|
46
45
|
}
|
|
47
46
|
async init() {
|
|
48
|
-
const
|
|
49
|
-
|
|
47
|
+
const lastEventsState = await this.getLastEventData();
|
|
48
|
+
if (this.wsChannel != null)
|
|
49
|
+
await this.setupWebsocket();
|
|
50
|
+
await this.setupPoll(lastEventsState, (newState) => this.saveLastEventData(newState));
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
exports.StarknetChainEvents = StarknetChainEvents;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChainEvents, ClaimEvent, EventListener, InitializeEvent, RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent } from "@atomiqlabs/base";
|
|
2
2
|
import { StarknetSwapData } from "../swaps/StarknetSwapData";
|
|
3
3
|
import { StarknetSwapContract } from "../swaps/StarknetSwapContract";
|
|
4
|
-
import { BigNumberish, Provider } from "starknet";
|
|
4
|
+
import { BigNumberish, Provider, SubscriptionStarknetEventsEvent, WebSocketChannel } from "starknet";
|
|
5
5
|
import { StarknetAbiEvent } from "../contract/modules/StarknetContractEvents";
|
|
6
6
|
import { EscrowManagerAbiType } from "../swaps/EscrowManagerAbi";
|
|
7
7
|
import { ExtractAbiFunctionNames } from "abi-wan-kanabi/dist/kanabi";
|
|
@@ -15,23 +15,36 @@ export type StarknetTraceCall = {
|
|
|
15
15
|
entry_point_selector: string;
|
|
16
16
|
calls: StarknetTraceCall[];
|
|
17
17
|
};
|
|
18
|
+
export type StarknetEventListenerState = {
|
|
19
|
+
lastBlockNumber: number;
|
|
20
|
+
lastTxHash?: string;
|
|
21
|
+
};
|
|
18
22
|
/**
|
|
19
23
|
* Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
|
|
20
24
|
* out on some events if the network is unreliable, front-end systems should take this into consideration and not
|
|
21
25
|
* rely purely on events
|
|
22
26
|
*/
|
|
23
27
|
export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {
|
|
28
|
+
private eventsProcessing;
|
|
29
|
+
private processedEvents;
|
|
30
|
+
protected readonly Chain: StarknetChainInterface;
|
|
24
31
|
protected readonly listeners: EventListener<StarknetSwapData>[];
|
|
32
|
+
protected readonly wsChannel?: WebSocketChannel;
|
|
25
33
|
protected readonly provider: Provider;
|
|
26
34
|
protected readonly starknetSwapContract: StarknetSwapContract;
|
|
27
35
|
protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
|
|
28
36
|
protected readonly logger: import("../../utils/Utils").LoggerType;
|
|
37
|
+
protected escrowContractSubscription: SubscriptionStarknetEventsEvent;
|
|
38
|
+
protected spvVaultContractSubscription: SubscriptionStarknetEventsEvent;
|
|
29
39
|
protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType>;
|
|
30
40
|
protected initEntryPointSelector: bigint;
|
|
31
41
|
protected stopped: boolean;
|
|
32
42
|
protected pollIntervalSeconds: number;
|
|
33
43
|
private timeout;
|
|
34
44
|
constructor(chainInterface: StarknetChainInterface, starknetSwapContract: StarknetSwapContract, starknetSpvVaultContract: StarknetSpvVaultContract, pollIntervalSeconds?: number);
|
|
45
|
+
private getEventFingerprint;
|
|
46
|
+
private addProcessedEvent;
|
|
47
|
+
private isEventProcessed;
|
|
35
48
|
findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData;
|
|
36
49
|
/**
|
|
37
50
|
* Returns async getter for fetching on-demand initialize event swap data
|
|
@@ -58,25 +71,24 @@ export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetS
|
|
|
58
71
|
* @param currentBlockTimestamp
|
|
59
72
|
* @protected
|
|
60
73
|
*/
|
|
61
|
-
protected 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
|
|
62
|
-
protected checkEventsEcrowManager(
|
|
74
|
+
protected 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): Promise<void>;
|
|
75
|
+
protected checkEventsEcrowManager(currentBlock: {
|
|
63
76
|
timestamp: number;
|
|
64
77
|
block_number: number;
|
|
65
|
-
}): Promise<string>;
|
|
66
|
-
protected checkEventsSpvVaults(
|
|
78
|
+
}, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]>;
|
|
79
|
+
protected checkEventsSpvVaults(currentBlock: {
|
|
67
80
|
timestamp: number;
|
|
68
81
|
block_number: number;
|
|
69
|
-
}): Promise<string>;
|
|
70
|
-
protected checkEvents(
|
|
71
|
-
txHashes: string[];
|
|
72
|
-
blockNumber: number;
|
|
73
|
-
}>;
|
|
82
|
+
}, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]>;
|
|
83
|
+
protected checkEvents(lastState: StarknetEventListenerState[]): Promise<StarknetEventListenerState[]>;
|
|
74
84
|
/**
|
|
75
85
|
* Sets up event handlers listening for swap events over websocket
|
|
76
86
|
*
|
|
77
87
|
* @protected
|
|
78
88
|
*/
|
|
79
|
-
protected setupPoll(
|
|
89
|
+
protected setupPoll(lastState?: StarknetEventListenerState[], saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>): Promise<void>;
|
|
90
|
+
protected wsStarted: boolean;
|
|
91
|
+
protected setupWebsocket(): Promise<void>;
|
|
80
92
|
init(): Promise<void>;
|
|
81
93
|
stop(): Promise<void>;
|
|
82
94
|
registerListener(cbk: EventListener<StarknetSwapData>): void;
|
|
@@ -4,6 +4,10 @@ exports.StarknetChainEventsBrowser = void 0;
|
|
|
4
4
|
const base_1 = require("@atomiqlabs/base");
|
|
5
5
|
const Utils_1 = require("../../utils/Utils");
|
|
6
6
|
const starknet_1 = require("starknet");
|
|
7
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
8
|
+
const buffer_1 = require("buffer");
|
|
9
|
+
const PROCESSED_EVENTS_BACKLOG = 5000;
|
|
10
|
+
const LOGS_SLIDING_WINDOW = 60;
|
|
7
11
|
/**
|
|
8
12
|
* Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
|
|
9
13
|
* out on some events if the network is unreliable, front-end systems should take this into consideration and not
|
|
@@ -11,15 +15,37 @@ const starknet_1 = require("starknet");
|
|
|
11
15
|
*/
|
|
12
16
|
class StarknetChainEventsBrowser {
|
|
13
17
|
constructor(chainInterface, starknetSwapContract, starknetSpvVaultContract, pollIntervalSeconds = 5) {
|
|
18
|
+
this.eventsProcessing = {};
|
|
19
|
+
this.processedEvents = new Set();
|
|
14
20
|
this.listeners = [];
|
|
15
21
|
this.logger = (0, Utils_1.getLogger)("StarknetChainEventsBrowser: ");
|
|
16
22
|
this.initFunctionName = "initialize";
|
|
17
23
|
this.initEntryPointSelector = BigInt(starknet_1.hash.starknetKeccak(this.initFunctionName));
|
|
24
|
+
this.wsStarted = false;
|
|
25
|
+
this.Chain = chainInterface;
|
|
26
|
+
this.wsChannel = chainInterface.wsChannel;
|
|
18
27
|
this.provider = chainInterface.provider;
|
|
19
28
|
this.starknetSwapContract = starknetSwapContract;
|
|
20
29
|
this.starknetSpvVaultContract = starknetSpvVaultContract;
|
|
21
30
|
this.pollIntervalSeconds = pollIntervalSeconds;
|
|
22
31
|
}
|
|
32
|
+
getEventFingerprint(event) {
|
|
33
|
+
const eventData = buffer_1.Buffer.concat([
|
|
34
|
+
...event.keys.map(value => (0, Utils_1.bigNumberishToBuffer)(value, 64)),
|
|
35
|
+
...event.data.map(value => (0, Utils_1.bigNumberishToBuffer)(value, 64))
|
|
36
|
+
]);
|
|
37
|
+
const fingerprint = buffer_1.Buffer.from((0, sha2_1.sha256)(eventData));
|
|
38
|
+
return event.txHash + ":" + fingerprint.toString("hex");
|
|
39
|
+
}
|
|
40
|
+
addProcessedEvent(event) {
|
|
41
|
+
this.processedEvents.add(this.getEventFingerprint(event));
|
|
42
|
+
if (this.processedEvents.size > PROCESSED_EVENTS_BACKLOG)
|
|
43
|
+
this.processedEvents.delete(this.processedEvents.keys().next().value);
|
|
44
|
+
}
|
|
45
|
+
isEventProcessed(eventOrFingerprint) {
|
|
46
|
+
const eventFingerprint = typeof (eventOrFingerprint) === "string" ? eventOrFingerprint : this.getEventFingerprint(eventOrFingerprint);
|
|
47
|
+
return this.processedEvents.has(eventFingerprint);
|
|
48
|
+
}
|
|
23
49
|
findInitSwapData(call, escrowHash, claimHandler) {
|
|
24
50
|
if (BigInt(call.contract_address) === BigInt(this.starknetSwapContract.contract.address) &&
|
|
25
51
|
BigInt(call.entry_point_selector) === this.initEntryPointSelector) {
|
|
@@ -164,11 +190,15 @@ class StarknetChainEventsBrowser {
|
|
|
164
190
|
if (blockNumber === currentBlockNumber)
|
|
165
191
|
return currentBlockTimestamp;
|
|
166
192
|
const blockNumberString = blockNumber.toString();
|
|
167
|
-
blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] =
|
|
193
|
+
blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] = await this.Chain.Blocks.getBlockTime(blockNumber));
|
|
168
194
|
return blockTimestampsCache[blockNumberString];
|
|
169
195
|
};
|
|
170
|
-
const parsedEvents = [];
|
|
171
196
|
for (let event of events) {
|
|
197
|
+
const eventIdentifier = this.getEventFingerprint(event);
|
|
198
|
+
if (this.isEventProcessed(eventIdentifier)) {
|
|
199
|
+
this.logger.debug("processEvents(): skipping already processed event: " + eventIdentifier);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
172
202
|
let parsedEvent;
|
|
173
203
|
switch (event.name) {
|
|
174
204
|
case "escrow_manager::events::Claim":
|
|
@@ -196,84 +226,119 @@ class StarknetChainEventsBrowser {
|
|
|
196
226
|
parsedEvent = this.parseSpvCloseEvent(event);
|
|
197
227
|
break;
|
|
198
228
|
}
|
|
199
|
-
if (
|
|
229
|
+
if (this.eventsProcessing[eventIdentifier] != null) {
|
|
230
|
+
this.logger.debug("processEvents(): awaiting event that is currently processing: " + eventIdentifier);
|
|
231
|
+
await this.eventsProcessing[eventIdentifier];
|
|
200
232
|
continue;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
timestamp
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
}
|
|
234
|
+
const promise = (async () => {
|
|
235
|
+
if (parsedEvent == null)
|
|
236
|
+
return;
|
|
237
|
+
//We are not trusting pre-confs for events, so this shall never happen
|
|
238
|
+
if (event.blockNumber == null)
|
|
239
|
+
throw new Error("Event block number cannot be null!");
|
|
240
|
+
const timestamp = await getBlockTimestamp(event.blockNumber);
|
|
241
|
+
parsedEvent.meta = {
|
|
242
|
+
blockTime: timestamp,
|
|
243
|
+
txId: event.txHash,
|
|
244
|
+
timestamp //Maybe deprecated
|
|
245
|
+
};
|
|
246
|
+
for (let listener of this.listeners) {
|
|
247
|
+
await listener([parsedEvent]);
|
|
248
|
+
}
|
|
249
|
+
this.addProcessedEvent(event);
|
|
250
|
+
})();
|
|
251
|
+
this.eventsProcessing[eventIdentifier] = promise;
|
|
252
|
+
try {
|
|
253
|
+
await promise;
|
|
254
|
+
delete this.eventsProcessing[eventIdentifier];
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
delete this.eventsProcessing[eventIdentifier];
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
214
260
|
}
|
|
215
261
|
}
|
|
216
|
-
async checkEventsEcrowManager(lastTxHash, lastBlockNumber
|
|
262
|
+
async checkEventsEcrowManager(currentBlock, lastTxHash, lastBlockNumber) {
|
|
217
263
|
const currentBlockNumber = currentBlock.block_number;
|
|
218
264
|
lastBlockNumber ?? (lastBlockNumber = currentBlockNumber);
|
|
265
|
+
if (currentBlockNumber < lastBlockNumber) {
|
|
266
|
+
this.logger.warn(`checkEventsEscrowManager(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
219
269
|
// this.logger.debug("checkEvents(EscrowManager): Requesting logs: "+logStartHeight+"...pending");
|
|
220
270
|
let events = await this.starknetSwapContract.Events.getContractBlockEvents(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], [], lastBlockNumber, null);
|
|
221
271
|
if (lastTxHash != null) {
|
|
222
272
|
const latestProcessedEventIndex = (0, Utils_1.findLastIndex)(events, val => val.txHash === lastTxHash);
|
|
223
273
|
if (latestProcessedEventIndex !== -1) {
|
|
224
274
|
events.splice(0, latestProcessedEventIndex + 1);
|
|
225
|
-
|
|
275
|
+
this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: " + events.length);
|
|
226
276
|
}
|
|
227
277
|
}
|
|
228
278
|
if (events.length > 0) {
|
|
229
279
|
await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
|
|
230
|
-
|
|
280
|
+
const lastProcessed = events[events.length - 1];
|
|
281
|
+
lastTxHash = lastProcessed.txHash;
|
|
282
|
+
if (lastProcessed.blockNumber > lastBlockNumber)
|
|
283
|
+
lastBlockNumber = lastProcessed.blockNumber;
|
|
284
|
+
}
|
|
285
|
+
else if (currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
|
|
286
|
+
lastTxHash = null;
|
|
287
|
+
lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
|
|
231
288
|
}
|
|
232
|
-
return lastTxHash;
|
|
289
|
+
return [lastTxHash, lastBlockNumber];
|
|
233
290
|
}
|
|
234
|
-
async checkEventsSpvVaults(lastTxHash, lastBlockNumber
|
|
291
|
+
async checkEventsSpvVaults(currentBlock, lastTxHash, lastBlockNumber) {
|
|
235
292
|
const currentBlockNumber = currentBlock.block_number;
|
|
236
293
|
lastBlockNumber ?? (lastBlockNumber = currentBlockNumber);
|
|
294
|
+
if (currentBlockNumber < lastBlockNumber) {
|
|
295
|
+
this.logger.warn(`checkEventsSpvVaults(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
237
298
|
// this.logger.debug("checkEvents(SpvVaults): Requesting logs: "+logStartHeight+"...pending");
|
|
238
299
|
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);
|
|
239
300
|
if (lastTxHash != null) {
|
|
240
301
|
const latestProcessedEventIndex = (0, Utils_1.findLastIndex)(events, val => val.txHash === lastTxHash);
|
|
241
302
|
if (latestProcessedEventIndex !== -1) {
|
|
242
303
|
events.splice(0, latestProcessedEventIndex + 1);
|
|
243
|
-
|
|
304
|
+
this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: " + events.length);
|
|
244
305
|
}
|
|
245
306
|
}
|
|
246
307
|
if (events.length > 0) {
|
|
247
308
|
await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
|
|
248
|
-
|
|
309
|
+
const lastProcessed = events[events.length - 1];
|
|
310
|
+
lastTxHash = lastProcessed.txHash;
|
|
311
|
+
if (lastProcessed.blockNumber > lastBlockNumber)
|
|
312
|
+
lastBlockNumber = lastProcessed.blockNumber;
|
|
249
313
|
}
|
|
250
|
-
|
|
314
|
+
else if (currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
|
|
315
|
+
lastTxHash = null;
|
|
316
|
+
lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
|
|
317
|
+
}
|
|
318
|
+
return [lastTxHash, lastBlockNumber];
|
|
251
319
|
}
|
|
252
|
-
async checkEvents(
|
|
253
|
-
|
|
254
|
-
const currentBlock = await this.
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
320
|
+
async checkEvents(lastState) {
|
|
321
|
+
lastState ?? (lastState = []);
|
|
322
|
+
const currentBlock = await this.Chain.Blocks.getBlock(starknet_1.BlockTag.LATEST);
|
|
323
|
+
const [lastEscrowTxHash, lastEscrowHeight] = await this.checkEventsEcrowManager(currentBlock, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
|
|
324
|
+
const [lastSpvVaultTxHash, lastSpvVaultHeight] = await this.checkEventsSpvVaults(currentBlock, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
|
|
325
|
+
return [
|
|
326
|
+
{ lastBlockNumber: lastEscrowHeight, lastTxHash: lastEscrowTxHash },
|
|
327
|
+
{ lastBlockNumber: lastSpvVaultHeight, lastTxHash: lastSpvVaultTxHash }
|
|
328
|
+
];
|
|
262
329
|
}
|
|
263
330
|
/**
|
|
264
331
|
* Sets up event handlers listening for swap events over websocket
|
|
265
332
|
*
|
|
266
333
|
* @protected
|
|
267
334
|
*/
|
|
268
|
-
async setupPoll(
|
|
269
|
-
this.stopped = false;
|
|
335
|
+
async setupPoll(lastState, saveLatestProcessedBlockNumber) {
|
|
270
336
|
let func;
|
|
271
337
|
func = async () => {
|
|
272
|
-
await this.checkEvents(
|
|
273
|
-
|
|
274
|
-
lastTxHashes = txHashes;
|
|
338
|
+
await this.checkEvents(lastState).then(newState => {
|
|
339
|
+
lastState = newState;
|
|
275
340
|
if (saveLatestProcessedBlockNumber != null)
|
|
276
|
-
return saveLatestProcessedBlockNumber(
|
|
341
|
+
return saveLatestProcessedBlockNumber(newState);
|
|
277
342
|
}).catch(e => {
|
|
278
343
|
this.logger.error("setupPoll(): Failed to fetch starknet log: ", e);
|
|
279
344
|
});
|
|
@@ -283,14 +348,49 @@ class StarknetChainEventsBrowser {
|
|
|
283
348
|
};
|
|
284
349
|
await func();
|
|
285
350
|
}
|
|
286
|
-
|
|
287
|
-
this.
|
|
288
|
-
|
|
351
|
+
async setupWebsocket() {
|
|
352
|
+
this.wsStarted = true;
|
|
353
|
+
const [escrowContractSubscription, spvVaultContractSubscription] = await Promise.all([
|
|
354
|
+
this.wsChannel.subscribeEvents({
|
|
355
|
+
fromAddress: this.starknetSwapContract.contract.address,
|
|
356
|
+
keys: this.starknetSwapContract.Events.toFilter(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], []),
|
|
357
|
+
finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2
|
|
358
|
+
}),
|
|
359
|
+
this.wsChannel.subscribeEvents({
|
|
360
|
+
fromAddress: this.starknetSpvVaultContract.contract.address,
|
|
361
|
+
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"], []),
|
|
362
|
+
finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2
|
|
363
|
+
})
|
|
364
|
+
]);
|
|
365
|
+
escrowContractSubscription.on((event) => {
|
|
366
|
+
const parsedEvents = this.starknetSwapContract.Events.toStarknetAbiEvents([event]);
|
|
367
|
+
this.processEvents(parsedEvents, event.block_number);
|
|
368
|
+
});
|
|
369
|
+
this.escrowContractSubscription = escrowContractSubscription;
|
|
370
|
+
spvVaultContractSubscription.on((event) => {
|
|
371
|
+
const parsedEvents = this.starknetSpvVaultContract.Events.toStarknetAbiEvents([event]);
|
|
372
|
+
this.processEvents(parsedEvents, event.block_number);
|
|
373
|
+
});
|
|
374
|
+
this.spvVaultContractSubscription = spvVaultContractSubscription;
|
|
375
|
+
}
|
|
376
|
+
async init() {
|
|
377
|
+
if (this.wsChannel != null) {
|
|
378
|
+
await this.setupWebsocket();
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
await this.setupPoll();
|
|
382
|
+
}
|
|
383
|
+
this.stopped = false;
|
|
289
384
|
}
|
|
290
385
|
async stop() {
|
|
291
386
|
this.stopped = true;
|
|
292
387
|
if (this.timeout != null)
|
|
293
388
|
clearTimeout(this.timeout);
|
|
389
|
+
if (this.wsStarted) {
|
|
390
|
+
await this.escrowContractSubscription.unsubscribe();
|
|
391
|
+
await this.spvVaultContractSubscription.unsubscribe();
|
|
392
|
+
this.wsStarted = false;
|
|
393
|
+
}
|
|
294
394
|
}
|
|
295
395
|
registerListener(cbk) {
|
|
296
396
|
this.listeners.push(cbk);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomiqlabs/chain-starknet",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.5",
|
|
4
4
|
"description": "Starknet specific base implementation",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"@scure/btc-signer": "^1.6.0",
|
|
34
34
|
"abi-wan-kanabi": "2.2.4",
|
|
35
35
|
"buffer": "6.0.3",
|
|
36
|
-
"promise-queue-ts": "^1.0.0"
|
|
36
|
+
"promise-queue-ts": "^1.0.0",
|
|
37
|
+
"ws": "^8.18.3"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
40
|
"starknet": "^8.5.0"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {constants, Provider} from "starknet";
|
|
1
|
+
import {constants, Provider, WebSocketChannel} from "starknet";
|
|
2
2
|
import {StarknetFees} from "./chain/modules/StarknetFees";
|
|
3
3
|
import {StarknetChainInterface, StarknetConfig, StarknetRetryPolicy} from "./chain/StarknetChainInterface";
|
|
4
4
|
import {StarknetBtcRelay} from "./btcrelay/StarknetBtcRelay";
|
|
@@ -41,6 +41,7 @@ export const StarknetAssets: StarknetAssetsType = {
|
|
|
41
41
|
|
|
42
42
|
export type StarknetOptions = {
|
|
43
43
|
rpcUrl: string | Provider,
|
|
44
|
+
wsUrl?: string | WebSocketChannel,
|
|
44
45
|
retryPolicy?: StarknetRetryPolicy,
|
|
45
46
|
chainId?: constants.StarknetChainId,
|
|
46
47
|
|
|
@@ -69,13 +70,17 @@ export function initializeStarknet(
|
|
|
69
70
|
const provider = typeof(options.rpcUrl)==="string" ?
|
|
70
71
|
new RpcProviderWithRetries({nodeUrl: options.rpcUrl}) :
|
|
71
72
|
options.rpcUrl;
|
|
73
|
+
let wsChannel: WebSocketChannel;
|
|
74
|
+
if(options.wsUrl!=null) wsChannel = typeof(options.wsUrl)==="string" ?
|
|
75
|
+
new WebSocketChannel({nodeUrl: options.wsUrl, websocket: typeof window !== "undefined" && typeof window.WebSocket !== "undefined" ? window.WebSocket : require("ws")}) :
|
|
76
|
+
options.wsUrl;
|
|
72
77
|
|
|
73
78
|
const Fees = options.fees ?? new StarknetFees(provider);
|
|
74
79
|
|
|
75
80
|
const chainId = options.chainId ??
|
|
76
81
|
(network===BitcoinNetwork.MAINNET ? constants.StarknetChainId.SN_MAIN : constants.StarknetChainId.SN_SEPOLIA);
|
|
77
82
|
|
|
78
|
-
const chainInterface = new StarknetChainInterface(chainId, provider, options.retryPolicy, Fees, options.starknetConfig);
|
|
83
|
+
const chainInterface = new StarknetChainInterface(chainId, provider, wsChannel, options.retryPolicy, Fees, options.starknetConfig);
|
|
79
84
|
|
|
80
85
|
const btcRelay = new StarknetBtcRelay(
|
|
81
86
|
chainInterface, bitcoinRpc, network, options.btcRelayContract
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Provider, constants, stark, ec, Account, provider, wallet} from "starknet";
|
|
1
|
+
import {Provider, constants, stark, ec, Account, provider, wallet, WebSocketChannel} from "starknet";
|
|
2
2
|
import {getLogger, toHex} from "../../utils/Utils";
|
|
3
3
|
import {StarknetTransactions, StarknetTx} from "./modules/StarknetTransactions";
|
|
4
4
|
import {StarknetFees} from "./modules/StarknetFees";
|
|
@@ -32,6 +32,7 @@ export class StarknetChainInterface implements ChainInterface<StarknetTx, Starkn
|
|
|
32
32
|
|
|
33
33
|
readonly chainId = "STARKNET";
|
|
34
34
|
|
|
35
|
+
readonly wsChannel?: WebSocketChannel;
|
|
35
36
|
readonly provider: Provider;
|
|
36
37
|
readonly retryPolicy: StarknetRetryPolicy;
|
|
37
38
|
|
|
@@ -52,6 +53,7 @@ export class StarknetChainInterface implements ChainInterface<StarknetTx, Starkn
|
|
|
52
53
|
constructor(
|
|
53
54
|
chainId: constants.StarknetChainId,
|
|
54
55
|
provider: Provider,
|
|
56
|
+
wsChannel?: WebSocketChannel,
|
|
55
57
|
retryPolicy?: StarknetRetryPolicy,
|
|
56
58
|
feeEstimator: StarknetFees = new StarknetFees(provider),
|
|
57
59
|
options?: StarknetConfig
|
|
@@ -64,6 +66,7 @@ export class StarknetChainInterface implements ChainInterface<StarknetTx, Starkn
|
|
|
64
66
|
this.config.getLogChunkSize ??= 100;
|
|
65
67
|
this.config.maxGetLogKeys ??= 64;
|
|
66
68
|
this.config.maxParallelCalls ??= 10;
|
|
69
|
+
this.wsChannel = wsChannel;
|
|
67
70
|
|
|
68
71
|
this.Fees = feeEstimator;
|
|
69
72
|
this.Tokens = new StarknetTokens(this);
|
|
@@ -27,7 +27,7 @@ export class StarknetContractEvents<TAbi extends Abi> extends StarknetEvents {
|
|
|
27
27
|
this.abi = abi;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
public toStarknetAbiEvents<T extends ExtractAbiEventNames<TAbi>>(blockEvents: StarknetEvent[]): StarknetAbiEvent<TAbi, T>[] {
|
|
31
31
|
const abiEvents = events.getAbiEvents(this.abi);
|
|
32
32
|
const abiStructs = CallData.getAbiStruct(this.abi);
|
|
33
33
|
const abiEnums = CallData.getAbiEnum(this.abi);
|
|
@@ -49,7 +49,7 @@ export class StarknetContractEvents<TAbi extends Abi> extends StarknetEvents {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
public toFilter<T extends ExtractAbiEventNames<TAbi>>(
|
|
53
53
|
events: T[],
|
|
54
54
|
keys: (string | string[])[],
|
|
55
55
|
): string[][] {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {StarknetChainEventsBrowser} from "./StarknetChainEventsBrowser";
|
|
1
|
+
import {StarknetChainEventsBrowser, StarknetEventListenerState} from "./StarknetChainEventsBrowser";
|
|
2
2
|
//@ts-ignore
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import {StarknetSwapContract} from "../swaps/StarknetSwapContract";
|
|
@@ -27,23 +27,24 @@ export class StarknetChainEvents extends StarknetChainEventsBrowser {
|
|
|
27
27
|
*
|
|
28
28
|
* @private
|
|
29
29
|
*/
|
|
30
|
-
private async getLastEventData(): Promise<
|
|
30
|
+
private async getLastEventData(): Promise<StarknetEventListenerState[]> {
|
|
31
31
|
try {
|
|
32
32
|
const txt: string = (await fs.readFile(this.directory+BLOCKHEIGHT_FILENAME)).toString();
|
|
33
|
-
const arr = txt.split("
|
|
34
|
-
if(arr.length<2)
|
|
35
|
-
blockNumber
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
const arr = txt.split(",");
|
|
34
|
+
if(arr.length<2) {
|
|
35
|
+
const blockNumber = parseInt(arr[0].split(";")[0]);
|
|
36
|
+
return [
|
|
37
|
+
{lastBlockNumber: blockNumber, lastTxHash: null},
|
|
38
|
+
{lastBlockNumber: blockNumber, lastTxHash: null}
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return arr.map(arrValue => {
|
|
43
|
+
const subArray = arrValue.split(";");
|
|
44
|
+
return {lastBlockNumber: parseInt(subArray[0]), lastTxHash: subArray[1]};
|
|
45
|
+
})
|
|
42
46
|
} catch (e) {
|
|
43
|
-
return
|
|
44
|
-
blockNumber: null,
|
|
45
|
-
txHashes: null
|
|
46
|
-
};
|
|
47
|
+
return [];
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -52,16 +53,16 @@ export class StarknetChainEvents extends StarknetChainEventsBrowser {
|
|
|
52
53
|
*
|
|
53
54
|
* @private
|
|
54
55
|
*/
|
|
55
|
-
private saveLastEventData(
|
|
56
|
-
return fs.writeFile(this.directory+BLOCKHEIGHT_FILENAME,
|
|
56
|
+
private saveLastEventData(newState: StarknetEventListenerState[]): Promise<void> {
|
|
57
|
+
return fs.writeFile(this.directory+BLOCKHEIGHT_FILENAME, newState.map(value => value.lastTxHash==null ? value.lastBlockNumber.toString(10) : value.lastBlockNumber.toString(10)+";"+value.lastTxHash).join(","));
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async init(): Promise<void> {
|
|
60
|
-
const
|
|
61
|
+
const lastEventsState = await this.getLastEventData();
|
|
62
|
+
if(this.wsChannel!=null) await this.setupWebsocket();
|
|
61
63
|
await this.setupPoll(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
(blockNumber: number, txHashes: string[]) => this.saveLastEventData(blockNumber, txHashes)
|
|
64
|
+
lastEventsState,
|
|
65
|
+
(newState: StarknetEventListenerState[]) => this.saveLastEventData(newState)
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -17,7 +17,15 @@ import {
|
|
|
17
17
|
toHex
|
|
18
18
|
} from "../../utils/Utils";
|
|
19
19
|
import {StarknetSwapContract} from "../swaps/StarknetSwapContract";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
BigNumberish,
|
|
22
|
+
BlockTag,
|
|
23
|
+
hash,
|
|
24
|
+
Provider,
|
|
25
|
+
SubscriptionStarknetEventsEvent,
|
|
26
|
+
TransactionFinalityStatus,
|
|
27
|
+
WebSocketChannel
|
|
28
|
+
} from "starknet";
|
|
21
29
|
import {StarknetAbiEvent} from "../contract/modules/StarknetContractEvents";
|
|
22
30
|
import {EscrowManagerAbiType} from "../swaps/EscrowManagerAbi";
|
|
23
31
|
import {ExtractAbiFunctionNames} from "abi-wan-kanabi/dist/kanabi";
|
|
@@ -25,6 +33,11 @@ import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";
|
|
|
25
33
|
import {StarknetSpvVaultContract} from "../spv_swap/StarknetSpvVaultContract";
|
|
26
34
|
import {StarknetChainInterface} from "../chain/StarknetChainInterface";
|
|
27
35
|
import {SpvVaultContractAbiType} from "../spv_swap/SpvVaultContractAbi";
|
|
36
|
+
import {sha256} from "@noble/hashes/sha2";
|
|
37
|
+
import {Buffer} from "buffer";
|
|
38
|
+
|
|
39
|
+
const PROCESSED_EVENTS_BACKLOG = 5000;
|
|
40
|
+
const LOGS_SLIDING_WINDOW = 60;
|
|
28
41
|
|
|
29
42
|
export type StarknetTraceCall = {
|
|
30
43
|
calldata: string[],
|
|
@@ -33,6 +46,8 @@ export type StarknetTraceCall = {
|
|
|
33
46
|
calls: StarknetTraceCall[]
|
|
34
47
|
};
|
|
35
48
|
|
|
49
|
+
export type StarknetEventListenerState = {lastBlockNumber: number, lastTxHash?: string};
|
|
50
|
+
|
|
36
51
|
/**
|
|
37
52
|
* Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
|
|
38
53
|
* out on some events if the network is unreliable, front-end systems should take this into consideration and not
|
|
@@ -40,12 +55,22 @@ export type StarknetTraceCall = {
|
|
|
40
55
|
*/
|
|
41
56
|
export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {
|
|
42
57
|
|
|
58
|
+
private eventsProcessing: {
|
|
59
|
+
[eventFingerprint: string]: Promise<void>
|
|
60
|
+
} = {};
|
|
61
|
+
private processedEvents: Set<string> = new Set();
|
|
62
|
+
|
|
63
|
+
protected readonly Chain: StarknetChainInterface;
|
|
43
64
|
protected readonly listeners: EventListener<StarknetSwapData>[] = [];
|
|
65
|
+
protected readonly wsChannel?: WebSocketChannel;
|
|
44
66
|
protected readonly provider: Provider;
|
|
45
67
|
protected readonly starknetSwapContract: StarknetSwapContract;
|
|
46
68
|
protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
|
|
47
69
|
protected readonly logger = getLogger("StarknetChainEventsBrowser: ");
|
|
48
70
|
|
|
71
|
+
protected escrowContractSubscription: SubscriptionStarknetEventsEvent;
|
|
72
|
+
protected spvVaultContractSubscription: SubscriptionStarknetEventsEvent;
|
|
73
|
+
|
|
49
74
|
protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType> = "initialize";
|
|
50
75
|
protected initEntryPointSelector = BigInt(hash.starknetKeccak(this.initFunctionName));
|
|
51
76
|
|
|
@@ -60,12 +85,34 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
60
85
|
starknetSpvVaultContract: StarknetSpvVaultContract,
|
|
61
86
|
pollIntervalSeconds: number = 5
|
|
62
87
|
) {
|
|
88
|
+
this.Chain = chainInterface;
|
|
89
|
+
this.wsChannel = chainInterface.wsChannel;
|
|
63
90
|
this.provider = chainInterface.provider;
|
|
64
91
|
this.starknetSwapContract = starknetSwapContract;
|
|
65
92
|
this.starknetSpvVaultContract = starknetSpvVaultContract;
|
|
66
93
|
this.pollIntervalSeconds = pollIntervalSeconds;
|
|
67
94
|
}
|
|
68
95
|
|
|
96
|
+
private getEventFingerprint(event: {keys: string[], data: string[], txHash: string}): string {
|
|
97
|
+
const eventData = Buffer.concat([
|
|
98
|
+
...event.keys.map(value => bigNumberishToBuffer(value, 64)),
|
|
99
|
+
...event.data.map(value => bigNumberishToBuffer(value, 64))
|
|
100
|
+
]);
|
|
101
|
+
const fingerprint = Buffer.from(sha256(eventData));
|
|
102
|
+
|
|
103
|
+
return event.txHash+":"+fingerprint.toString("hex");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private addProcessedEvent(event: {keys: string[], data: string[], txHash: string}) {
|
|
107
|
+
this.processedEvents.add(this.getEventFingerprint(event));
|
|
108
|
+
if(this.processedEvents.size > PROCESSED_EVENTS_BACKLOG) this.processedEvents.delete(this.processedEvents.keys().next().value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private isEventProcessed(eventOrFingerprint: {keys: string[], data: string[], txHash: string} | string): boolean {
|
|
112
|
+
const eventFingerprint: string = typeof(eventOrFingerprint)==="string" ? eventOrFingerprint : this.getEventFingerprint(eventOrFingerprint);
|
|
113
|
+
return this.processedEvents.has(eventFingerprint);
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData {
|
|
70
117
|
if(
|
|
71
118
|
BigInt(call.contract_address)===BigInt(this.starknetSwapContract.contract.address) &&
|
|
@@ -249,19 +296,24 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
249
296
|
"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"
|
|
250
297
|
>)[],
|
|
251
298
|
currentBlockNumber: number,
|
|
252
|
-
currentBlockTimestamp
|
|
299
|
+
currentBlockTimestamp?: number
|
|
253
300
|
) {
|
|
254
301
|
const blockTimestampsCache: {[blockNumber: string]: number} = {};
|
|
255
302
|
const getBlockTimestamp: (blockNumber: number) => Promise<number> = async (blockNumber: number)=> {
|
|
256
303
|
if(blockNumber===currentBlockNumber) return currentBlockTimestamp;
|
|
257
304
|
const blockNumberString = blockNumber.toString();
|
|
258
|
-
blockTimestampsCache[blockNumberString] ??=
|
|
305
|
+
blockTimestampsCache[blockNumberString] ??= await this.Chain.Blocks.getBlockTime(blockNumber);
|
|
259
306
|
return blockTimestampsCache[blockNumberString];
|
|
260
307
|
}
|
|
261
308
|
|
|
262
|
-
const parsedEvents: ChainEvent<StarknetSwapData>[] = [];
|
|
263
|
-
|
|
264
309
|
for(let event of events) {
|
|
310
|
+
const eventIdentifier = this.getEventFingerprint(event);
|
|
311
|
+
|
|
312
|
+
if(this.isEventProcessed(eventIdentifier)) {
|
|
313
|
+
this.logger.debug("processEvents(): skipping already processed event: "+eventIdentifier);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
265
317
|
let parsedEvent: ChainEvent<StarknetSwapData>;
|
|
266
318
|
switch(event.name) {
|
|
267
319
|
case "escrow_manager::events::Claim":
|
|
@@ -289,26 +341,47 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
289
341
|
parsedEvent = this.parseSpvCloseEvent(event as any);
|
|
290
342
|
break;
|
|
291
343
|
}
|
|
292
|
-
if(parsedEvent==null) continue;
|
|
293
|
-
//We are not trusting pre-confs for events, so this shall never happen
|
|
294
|
-
if(event.blockNumber==null) throw new Error("Event block number cannot be null!");
|
|
295
|
-
const timestamp = await getBlockTimestamp(event.blockNumber);
|
|
296
|
-
parsedEvent.meta = {
|
|
297
|
-
blockTime: timestamp,
|
|
298
|
-
txId: event.txHash,
|
|
299
|
-
timestamp //Maybe deprecated
|
|
300
|
-
} as any;
|
|
301
|
-
parsedEvents.push(parsedEvent);
|
|
302
|
-
}
|
|
303
344
|
|
|
304
|
-
|
|
305
|
-
|
|
345
|
+
if(this.eventsProcessing[eventIdentifier]!=null) {
|
|
346
|
+
this.logger.debug("processEvents(): awaiting event that is currently processing: "+eventIdentifier);
|
|
347
|
+
await this.eventsProcessing[eventIdentifier];
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const promise = (async() => {
|
|
352
|
+
if(parsedEvent==null) return;
|
|
353
|
+
//We are not trusting pre-confs for events, so this shall never happen
|
|
354
|
+
if(event.blockNumber==null) throw new Error("Event block number cannot be null!");
|
|
355
|
+
const timestamp = await getBlockTimestamp(event.blockNumber);
|
|
356
|
+
parsedEvent.meta = {
|
|
357
|
+
blockTime: timestamp,
|
|
358
|
+
txId: event.txHash,
|
|
359
|
+
timestamp //Maybe deprecated
|
|
360
|
+
} as any;
|
|
361
|
+
for(let listener of this.listeners) {
|
|
362
|
+
await listener([parsedEvent]);
|
|
363
|
+
}
|
|
364
|
+
this.addProcessedEvent(event);
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
this.eventsProcessing[eventIdentifier] = promise;
|
|
368
|
+
try {
|
|
369
|
+
await promise;
|
|
370
|
+
delete this.eventsProcessing[eventIdentifier];
|
|
371
|
+
} catch (e) {
|
|
372
|
+
delete this.eventsProcessing[eventIdentifier];
|
|
373
|
+
throw e;
|
|
374
|
+
}
|
|
306
375
|
}
|
|
307
376
|
}
|
|
308
377
|
|
|
309
|
-
protected async checkEventsEcrowManager(
|
|
310
|
-
const currentBlockNumber: number =
|
|
378
|
+
protected async checkEventsEcrowManager(currentBlock: {timestamp: number, block_number: number}, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]> {
|
|
379
|
+
const currentBlockNumber: number = currentBlock.block_number;
|
|
311
380
|
lastBlockNumber ??= currentBlockNumber;
|
|
381
|
+
if(currentBlockNumber < lastBlockNumber) {
|
|
382
|
+
this.logger.warn(`checkEventsEscrowManager(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
312
385
|
// this.logger.debug("checkEvents(EscrowManager): Requesting logs: "+logStartHeight+"...pending");
|
|
313
386
|
let events = await this.starknetSwapContract.Events.getContractBlockEvents(
|
|
314
387
|
["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
|
|
@@ -320,19 +393,28 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
320
393
|
const latestProcessedEventIndex = findLastIndex(events, val => val.txHash===lastTxHash);
|
|
321
394
|
if(latestProcessedEventIndex!==-1) {
|
|
322
395
|
events.splice(0, latestProcessedEventIndex+1);
|
|
323
|
-
|
|
396
|
+
this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: "+events.length);
|
|
324
397
|
}
|
|
325
398
|
}
|
|
326
399
|
if(events.length>0) {
|
|
327
400
|
await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
|
|
328
|
-
|
|
401
|
+
const lastProcessed = events[events.length-1];
|
|
402
|
+
lastTxHash = lastProcessed.txHash;
|
|
403
|
+
if(lastProcessed.blockNumber > lastBlockNumber) lastBlockNumber = lastProcessed.blockNumber;
|
|
404
|
+
} else if(currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
|
|
405
|
+
lastTxHash = null;
|
|
406
|
+
lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
|
|
329
407
|
}
|
|
330
|
-
return lastTxHash;
|
|
408
|
+
return [lastTxHash, lastBlockNumber];
|
|
331
409
|
}
|
|
332
410
|
|
|
333
|
-
protected async checkEventsSpvVaults(
|
|
334
|
-
const currentBlockNumber: number =
|
|
411
|
+
protected async checkEventsSpvVaults(currentBlock: {timestamp: number, block_number: number}, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]> {
|
|
412
|
+
const currentBlockNumber: number = currentBlock.block_number;
|
|
335
413
|
lastBlockNumber ??= currentBlockNumber;
|
|
414
|
+
if(currentBlockNumber < lastBlockNumber) {
|
|
415
|
+
this.logger.warn(`checkEventsSpvVaults(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
336
418
|
// this.logger.debug("checkEvents(SpvVaults): Requesting logs: "+logStartHeight+"...pending");
|
|
337
419
|
let events = await this.starknetSpvVaultContract.Events.getContractBlockEvents(
|
|
338
420
|
["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"],
|
|
@@ -344,29 +426,33 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
344
426
|
const latestProcessedEventIndex = findLastIndex(events, val => val.txHash===lastTxHash);
|
|
345
427
|
if(latestProcessedEventIndex!==-1) {
|
|
346
428
|
events.splice(0, latestProcessedEventIndex+1);
|
|
347
|
-
|
|
429
|
+
this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: "+events.length);
|
|
348
430
|
}
|
|
349
431
|
}
|
|
350
432
|
if(events.length>0) {
|
|
351
433
|
await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
|
|
352
|
-
|
|
434
|
+
const lastProcessed = events[events.length-1];
|
|
435
|
+
lastTxHash = lastProcessed.txHash;
|
|
436
|
+
if(lastProcessed.blockNumber > lastBlockNumber) lastBlockNumber = lastProcessed.blockNumber;
|
|
437
|
+
} else if(currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
|
|
438
|
+
lastTxHash = null;
|
|
439
|
+
lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
|
|
353
440
|
}
|
|
354
|
-
return lastTxHash;
|
|
441
|
+
return [lastTxHash, lastBlockNumber];
|
|
355
442
|
}
|
|
356
443
|
|
|
357
|
-
protected async checkEvents(
|
|
358
|
-
|
|
444
|
+
protected async checkEvents(lastState: StarknetEventListenerState[]): Promise<StarknetEventListenerState[]> {
|
|
445
|
+
lastState ??= [];
|
|
359
446
|
|
|
360
|
-
const currentBlock = await this.
|
|
361
|
-
const currentBlockNumber: number = currentBlock.block_number;
|
|
447
|
+
const currentBlock = await this.Chain.Blocks.getBlock(BlockTag.LATEST);
|
|
362
448
|
|
|
363
|
-
|
|
364
|
-
|
|
449
|
+
const [lastEscrowTxHash, lastEscrowHeight] = await this.checkEventsEcrowManager(currentBlock as any, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
|
|
450
|
+
const [lastSpvVaultTxHash, lastSpvVaultHeight] = await this.checkEventsSpvVaults(currentBlock as any, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
|
|
365
451
|
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
452
|
+
return [
|
|
453
|
+
{lastBlockNumber: lastEscrowHeight, lastTxHash: lastEscrowTxHash},
|
|
454
|
+
{lastBlockNumber: lastSpvVaultHeight, lastTxHash: lastSpvVaultTxHash}
|
|
455
|
+
];
|
|
370
456
|
}
|
|
371
457
|
|
|
372
458
|
/**
|
|
@@ -375,17 +461,14 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
375
461
|
* @protected
|
|
376
462
|
*/
|
|
377
463
|
protected async setupPoll(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
saveLatestProcessedBlockNumber?: (blockNumber: number, lastTxHashes: string[]) => Promise<void>
|
|
464
|
+
lastState?: StarknetEventListenerState[],
|
|
465
|
+
saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>
|
|
381
466
|
) {
|
|
382
|
-
this.stopped = false;
|
|
383
467
|
let func;
|
|
384
468
|
func = async () => {
|
|
385
|
-
await this.checkEvents(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if(saveLatestProcessedBlockNumber!=null) return saveLatestProcessedBlockNumber(blockNumber, lastTxHashes);
|
|
469
|
+
await this.checkEvents(lastState).then(newState => {
|
|
470
|
+
lastState = newState;
|
|
471
|
+
if(saveLatestProcessedBlockNumber!=null) return saveLatestProcessedBlockNumber(newState);
|
|
389
472
|
}).catch(e => {
|
|
390
473
|
this.logger.error("setupPoll(): Failed to fetch starknet log: ", e);
|
|
391
474
|
});
|
|
@@ -395,14 +478,67 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
|
|
|
395
478
|
await func();
|
|
396
479
|
}
|
|
397
480
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
481
|
+
protected wsStarted: boolean = false;
|
|
482
|
+
|
|
483
|
+
protected async setupWebsocket() {
|
|
484
|
+
this.wsStarted = true;
|
|
485
|
+
|
|
486
|
+
const [
|
|
487
|
+
escrowContractSubscription,
|
|
488
|
+
spvVaultContractSubscription
|
|
489
|
+
] = await Promise.all([
|
|
490
|
+
this.wsChannel.subscribeEvents({
|
|
491
|
+
fromAddress: this.starknetSwapContract.contract.address,
|
|
492
|
+
keys: this.starknetSwapContract.Events.toFilter(
|
|
493
|
+
["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
|
|
494
|
+
[]
|
|
495
|
+
),
|
|
496
|
+
finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L2
|
|
497
|
+
}),
|
|
498
|
+
this.wsChannel.subscribeEvents({
|
|
499
|
+
fromAddress: this.starknetSpvVaultContract.contract.address,
|
|
500
|
+
keys: this.starknetSpvVaultContract.Events.toFilter(
|
|
501
|
+
["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"],
|
|
502
|
+
[]
|
|
503
|
+
),
|
|
504
|
+
finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L2
|
|
505
|
+
})
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
escrowContractSubscription.on((event) => {
|
|
509
|
+
const parsedEvents = this.starknetSwapContract.Events.toStarknetAbiEvents<
|
|
510
|
+
"escrow_manager::events::Initialize" | "escrow_manager::events::Claim" | "escrow_manager::events::Refund"
|
|
511
|
+
>([event]);
|
|
512
|
+
this.processEvents(parsedEvents, event.block_number);
|
|
513
|
+
});
|
|
514
|
+
this.escrowContractSubscription = escrowContractSubscription;
|
|
515
|
+
|
|
516
|
+
spvVaultContractSubscription.on((event) => {
|
|
517
|
+
const parsedEvents = this.starknetSpvVaultContract.Events.toStarknetAbiEvents<
|
|
518
|
+
"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"
|
|
519
|
+
>([event]);
|
|
520
|
+
this.processEvents(parsedEvents, event.block_number);
|
|
521
|
+
});
|
|
522
|
+
this.spvVaultContractSubscription = spvVaultContractSubscription;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async init(): Promise<void> {
|
|
526
|
+
if(this.wsChannel!=null) {
|
|
527
|
+
await this.setupWebsocket();
|
|
528
|
+
} else {
|
|
529
|
+
await this.setupPoll();
|
|
530
|
+
}
|
|
531
|
+
this.stopped = false;
|
|
401
532
|
}
|
|
402
533
|
|
|
403
534
|
async stop(): Promise<void> {
|
|
404
535
|
this.stopped = true;
|
|
405
536
|
if(this.timeout!=null) clearTimeout(this.timeout);
|
|
537
|
+
if(this.wsStarted) {
|
|
538
|
+
await this.escrowContractSubscription.unsubscribe();
|
|
539
|
+
await this.spvVaultContractSubscription.unsubscribe();
|
|
540
|
+
this.wsStarted = false;
|
|
541
|
+
}
|
|
406
542
|
}
|
|
407
543
|
|
|
408
544
|
registerListener(cbk: EventListener<StarknetSwapData>): void {
|