@atomiqlabs/chain-solana 12.0.12 → 12.0.14
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/LICENSE +201 -201
- package/dist/index.d.ts +29 -29
- package/dist/index.js +45 -45
- package/dist/solana/SolanaChainType.d.ts +11 -11
- package/dist/solana/SolanaChainType.js +2 -2
- package/dist/solana/SolanaChains.d.ts +20 -20
- package/dist/solana/SolanaChains.js +25 -25
- package/dist/solana/SolanaInitializer.d.ts +18 -18
- package/dist/solana/SolanaInitializer.js +63 -63
- package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +228 -228
- package/dist/solana/btcrelay/SolanaBtcRelay.js +441 -441
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +29 -29
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +34 -34
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +46 -46
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +78 -78
- package/dist/solana/btcrelay/program/programIdl.json +671 -671
- package/dist/solana/chain/SolanaAction.d.ts +26 -26
- package/dist/solana/chain/SolanaAction.js +86 -86
- package/dist/solana/chain/SolanaChainInterface.d.ts +65 -65
- package/dist/solana/chain/SolanaChainInterface.js +125 -125
- package/dist/solana/chain/SolanaModule.d.ts +14 -14
- package/dist/solana/chain/SolanaModule.js +13 -13
- package/dist/solana/chain/modules/SolanaAddresses.d.ts +8 -8
- package/dist/solana/chain/modules/SolanaAddresses.js +22 -22
- package/dist/solana/chain/modules/SolanaBlocks.d.ts +28 -28
- package/dist/solana/chain/modules/SolanaBlocks.js +72 -72
- package/dist/solana/chain/modules/SolanaEvents.d.ts +68 -68
- package/dist/solana/chain/modules/SolanaEvents.js +238 -225
- package/dist/solana/chain/modules/SolanaFees.d.ts +121 -121
- package/dist/solana/chain/modules/SolanaFees.js +379 -379
- package/dist/solana/chain/modules/SolanaSignatures.d.ts +23 -23
- package/dist/solana/chain/modules/SolanaSignatures.js +39 -39
- package/dist/solana/chain/modules/SolanaSlots.d.ts +31 -31
- package/dist/solana/chain/modules/SolanaSlots.js +68 -68
- package/dist/solana/chain/modules/SolanaTokens.d.ts +136 -136
- package/dist/solana/chain/modules/SolanaTokens.js +248 -248
- package/dist/solana/chain/modules/SolanaTransactions.d.ts +124 -124
- package/dist/solana/chain/modules/SolanaTransactions.js +323 -323
- package/dist/solana/events/SolanaChainEvents.d.ts +88 -88
- package/dist/solana/events/SolanaChainEvents.js +256 -256
- package/dist/solana/events/SolanaChainEventsBrowser.d.ts +75 -75
- package/dist/solana/events/SolanaChainEventsBrowser.js +172 -172
- package/dist/solana/program/SolanaProgramBase.d.ts +40 -40
- package/dist/solana/program/SolanaProgramBase.js +43 -43
- package/dist/solana/program/SolanaProgramModule.d.ts +8 -8
- package/dist/solana/program/SolanaProgramModule.js +11 -11
- package/dist/solana/program/modules/SolanaProgramEvents.d.ts +53 -53
- package/dist/solana/program/modules/SolanaProgramEvents.js +114 -114
- package/dist/solana/swaps/SolanaSwapData.d.ts +71 -71
- package/dist/solana/swaps/SolanaSwapData.js +292 -292
- package/dist/solana/swaps/SolanaSwapModule.d.ts +10 -10
- package/dist/solana/swaps/SolanaSwapModule.js +11 -11
- package/dist/solana/swaps/SolanaSwapProgram.d.ts +224 -224
- package/dist/solana/swaps/SolanaSwapProgram.js +570 -570
- package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -11
- package/dist/solana/swaps/SwapTypeEnum.js +42 -42
- package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +94 -94
- package/dist/solana/swaps/modules/SolanaDataAccount.js +231 -231
- package/dist/solana/swaps/modules/SolanaLpVault.d.ts +71 -71
- package/dist/solana/swaps/modules/SolanaLpVault.js +173 -173
- package/dist/solana/swaps/modules/SwapClaim.d.ts +129 -129
- package/dist/solana/swaps/modules/SwapClaim.js +291 -291
- package/dist/solana/swaps/modules/SwapInit.d.ts +217 -217
- package/dist/solana/swaps/modules/SwapInit.js +519 -519
- package/dist/solana/swaps/modules/SwapRefund.d.ts +82 -82
- package/dist/solana/swaps/modules/SwapRefund.js +262 -262
- package/dist/solana/swaps/programIdl.json +945 -945
- package/dist/solana/swaps/programTypes.d.ts +943 -943
- package/dist/solana/swaps/programTypes.js +945 -945
- package/dist/solana/wallet/SolanaKeypairWallet.d.ts +9 -9
- package/dist/solana/wallet/SolanaKeypairWallet.js +33 -33
- package/dist/solana/wallet/SolanaSigner.d.ts +11 -11
- package/dist/solana/wallet/SolanaSigner.js +17 -17
- package/dist/utils/Utils.d.ts +53 -53
- package/dist/utils/Utils.js +170 -170
- package/package.json +41 -41
- package/src/index.ts +36 -36
- package/src/solana/SolanaChainType.ts +27 -27
- package/src/solana/SolanaChains.ts +23 -23
- package/src/solana/SolanaInitializer.ts +102 -102
- package/src/solana/btcrelay/SolanaBtcRelay.ts +589 -589
- package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +57 -57
- package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +102 -102
- package/src/solana/btcrelay/program/programIdl.json +670 -670
- package/src/solana/chain/SolanaAction.ts +108 -108
- package/src/solana/chain/SolanaChainInterface.ts +192 -192
- package/src/solana/chain/SolanaModule.ts +20 -20
- package/src/solana/chain/modules/SolanaAddresses.ts +20 -20
- package/src/solana/chain/modules/SolanaBlocks.ts +78 -78
- package/src/solana/chain/modules/SolanaEvents.ts +270 -256
- package/src/solana/chain/modules/SolanaFees.ts +450 -450
- package/src/solana/chain/modules/SolanaSignatures.ts +39 -39
- package/src/solana/chain/modules/SolanaSlots.ts +82 -82
- package/src/solana/chain/modules/SolanaTokens.ts +307 -307
- package/src/solana/chain/modules/SolanaTransactions.ts +365 -365
- package/src/solana/events/SolanaChainEvents.ts +299 -299
- package/src/solana/events/SolanaChainEventsBrowser.ts +209 -209
- package/src/solana/program/SolanaProgramBase.ts +79 -79
- package/src/solana/program/SolanaProgramModule.ts +15 -15
- package/src/solana/program/modules/SolanaProgramEvents.ts +155 -155
- package/src/solana/swaps/SolanaSwapData.ts +430 -430
- package/src/solana/swaps/SolanaSwapModule.ts +16 -16
- package/src/solana/swaps/SolanaSwapProgram.ts +854 -854
- package/src/solana/swaps/SwapTypeEnum.ts +29 -29
- package/src/solana/swaps/modules/SolanaDataAccount.ts +307 -307
- package/src/solana/swaps/modules/SolanaLpVault.ts +215 -215
- package/src/solana/swaps/modules/SwapClaim.ts +389 -389
- package/src/solana/swaps/modules/SwapInit.ts +663 -663
- package/src/solana/swaps/modules/SwapRefund.ts +323 -323
- package/src/solana/swaps/programIdl.json +944 -944
- package/src/solana/swaps/programTypes.ts +1885 -1885
- package/src/solana/wallet/SolanaKeypairWallet.ts +36 -36
- package/src/solana/wallet/SolanaSigner.ts +24 -24
- package/src/utils/Utils.ts +180 -180
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
import {ConfirmedSignatureInfo, Connection, ParsedTransactionWithMeta} from "@solana/web3.js";
|
|
2
|
-
import {IdlEvents} from "@coral-xyz/anchor";
|
|
3
|
-
import * as fs from "fs/promises";
|
|
4
|
-
import {SolanaSwapProgram} from "../swaps/SolanaSwapProgram";
|
|
5
|
-
import {EventObject, SolanaChainEventsBrowser} from "./SolanaChainEventsBrowser";
|
|
6
|
-
import {SwapProgram} from "../swaps/programTypes";
|
|
7
|
-
|
|
8
|
-
const BLOCKHEIGHT_FILENAME = "/blockheight.txt";
|
|
9
|
-
const LOG_FETCH_INTERVAL = 5*1000;
|
|
10
|
-
const LOG_FETCH_LIMIT = 500;
|
|
11
|
-
|
|
12
|
-
const PROCESSED_SIGNATURES_BACKLOG = 100;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Event handler for backend Node.js systems with access to fs, uses HTTP polling in combination with WS to not miss
|
|
16
|
-
* any events
|
|
17
|
-
*/
|
|
18
|
-
export class SolanaChainEvents extends SolanaChainEventsBrowser {
|
|
19
|
-
|
|
20
|
-
private readonly directory: string;
|
|
21
|
-
private readonly logFetchInterval: number;
|
|
22
|
-
private readonly logFetchLimit: number;
|
|
23
|
-
|
|
24
|
-
private signaturesProcessing: {
|
|
25
|
-
[signature: string]: Promise<boolean>
|
|
26
|
-
} = {};
|
|
27
|
-
private processedSignatures: string[] = [];
|
|
28
|
-
private processedSignaturesIndex: number = 0;
|
|
29
|
-
private stopped: boolean;
|
|
30
|
-
private timeout: NodeJS.Timeout;
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
directory: string,
|
|
34
|
-
connection: Connection,
|
|
35
|
-
solanaSwapProgram: SolanaSwapProgram,
|
|
36
|
-
logFetchInterval?: number,
|
|
37
|
-
logFetchLimit?: number
|
|
38
|
-
) {
|
|
39
|
-
super(connection, solanaSwapProgram)
|
|
40
|
-
this.directory = directory;
|
|
41
|
-
this.logFetchInterval = logFetchInterval || LOG_FETCH_INTERVAL;
|
|
42
|
-
this.logFetchLimit = logFetchLimit || LOG_FETCH_LIMIT;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private addProcessedSignature(signature: string) {
|
|
46
|
-
this.processedSignatures[this.processedSignaturesIndex] = signature;
|
|
47
|
-
this.processedSignaturesIndex += 1;
|
|
48
|
-
if(this.processedSignaturesIndex >= PROCESSED_SIGNATURES_BACKLOG) this.processedSignaturesIndex = 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private isSignatureProcessed(signature: string): boolean {
|
|
52
|
-
return this.processedSignatures.includes(signature);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Retrieves last signature & slot from filesystem
|
|
57
|
-
*
|
|
58
|
-
* @private
|
|
59
|
-
*/
|
|
60
|
-
private async getLastSignature(): Promise<{
|
|
61
|
-
signature: string,
|
|
62
|
-
slot: number
|
|
63
|
-
}> {
|
|
64
|
-
try {
|
|
65
|
-
const txt = (await fs.readFile(this.directory+BLOCKHEIGHT_FILENAME)).toString();
|
|
66
|
-
const arr = txt.split(";");
|
|
67
|
-
if(arr.length<2) return {
|
|
68
|
-
signature: txt,
|
|
69
|
-
slot: 0
|
|
70
|
-
};
|
|
71
|
-
return {
|
|
72
|
-
signature: arr[0],
|
|
73
|
-
slot: parseInt(arr[1])
|
|
74
|
-
};
|
|
75
|
-
} catch (e) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Saves last signature & slot to the filesystem
|
|
82
|
-
*
|
|
83
|
-
* @private
|
|
84
|
-
*/
|
|
85
|
-
private saveLastSignature(lastSignature: string, slot: number): Promise<void> {
|
|
86
|
-
return fs.writeFile(this.directory+BLOCKHEIGHT_FILENAME, lastSignature+";"+slot);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Parses EventObject from the transaction
|
|
91
|
-
*
|
|
92
|
-
* @param transaction
|
|
93
|
-
* @private
|
|
94
|
-
* @returns {EventObject} parsed event object
|
|
95
|
-
*/
|
|
96
|
-
private getEventObjectFromTransaction(transaction: ParsedTransactionWithMeta): EventObject {
|
|
97
|
-
if(transaction.meta.err!=null) return null;
|
|
98
|
-
|
|
99
|
-
const instructions = this.solanaSwapProgram.Events.decodeInstructions(transaction.transaction.message);
|
|
100
|
-
const events = this.solanaSwapProgram.Events.parseLogs(transaction.meta.logMessages);
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
instructions,
|
|
104
|
-
events,
|
|
105
|
-
blockTime: transaction.blockTime,
|
|
106
|
-
signature: transaction.transaction.signatures[0]
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Fetches transaction from the RPC, parses it to even object & processes it through event handler
|
|
112
|
-
*
|
|
113
|
-
* @param signature
|
|
114
|
-
* @private
|
|
115
|
-
* @returns {boolean} whether the operation was successful
|
|
116
|
-
*/
|
|
117
|
-
private async fetchTxAndProcessEvent(signature: string): Promise<boolean> {
|
|
118
|
-
try {
|
|
119
|
-
const transaction = await this.connection.getParsedTransaction(signature, {
|
|
120
|
-
commitment: "confirmed",
|
|
121
|
-
maxSupportedTransactionVersion: 1
|
|
122
|
-
});
|
|
123
|
-
if(transaction==null) return false;
|
|
124
|
-
|
|
125
|
-
const eventObject = this.getEventObjectFromTransaction(transaction);
|
|
126
|
-
if(eventObject==null) return true;
|
|
127
|
-
|
|
128
|
-
await this.processEvent(eventObject);
|
|
129
|
-
return true;
|
|
130
|
-
} catch (e) {
|
|
131
|
-
this.logger.error("fetchTxAndProcessEvent(): Error fetching transaction and processing event, signature: "+signature, e);
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Returns websocket event handler for specific event type
|
|
138
|
-
*
|
|
139
|
-
* @param name
|
|
140
|
-
* @protected
|
|
141
|
-
* @returns event handler to be passed to program's addEventListener function
|
|
142
|
-
*/
|
|
143
|
-
protected getWsEventHandler<E extends "InitializeEvent" | "RefundEvent" | "ClaimEvent">(
|
|
144
|
-
name: E
|
|
145
|
-
): (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => void {
|
|
146
|
-
return (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => {
|
|
147
|
-
if(this.signaturesProcessing[signature]!=null) return;
|
|
148
|
-
if(this.isSignatureProcessed(signature)) return;
|
|
149
|
-
|
|
150
|
-
this.logger.debug("getWsEventHandler("+name+"): Process signature: ", signature);
|
|
151
|
-
|
|
152
|
-
this.signaturesProcessing[signature] = this.processEvent({
|
|
153
|
-
events: [{name, data: data as any}],
|
|
154
|
-
instructions: null, //Instructions will be fetched on-demand if required
|
|
155
|
-
blockTime: Math.floor(Date.now()/1000),
|
|
156
|
-
signature
|
|
157
|
-
}).then(() => true).catch(e => {
|
|
158
|
-
this.logger.error("getWsEventHandler("+name+"): Error processing signature: "+signature, e);
|
|
159
|
-
return false;
|
|
160
|
-
});
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Gets all the new signatures from the last processed signature
|
|
166
|
-
*
|
|
167
|
-
* @param lastProcessedSignature
|
|
168
|
-
* @private
|
|
169
|
-
*/
|
|
170
|
-
private async getNewSignatures(lastProcessedSignature: {signature: string, slot: number}): Promise<ConfirmedSignatureInfo[]> {
|
|
171
|
-
let signatures: ConfirmedSignatureInfo[] = [];
|
|
172
|
-
|
|
173
|
-
let fetched = null;
|
|
174
|
-
while(fetched==null || fetched.length===this.logFetchLimit) {
|
|
175
|
-
if(signatures.length===0) {
|
|
176
|
-
fetched = await this.connection.getSignaturesForAddress(this.solanaSwapProgram.program.programId, {
|
|
177
|
-
until: lastProcessedSignature.signature,
|
|
178
|
-
limit: this.logFetchLimit
|
|
179
|
-
}, "confirmed");
|
|
180
|
-
//Check if newest returned signature (index 0) is older than the latest signature's slot, this is a sanity check
|
|
181
|
-
if(fetched.length>0 && fetched[0].slot<lastProcessedSignature.slot) {
|
|
182
|
-
this.logger.debug("getNewSignatures(): Sanity check triggered, returned signature slot height is older than latest!");
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
fetched = await this.connection.getSignaturesForAddress(this.solanaSwapProgram.program.programId, {
|
|
187
|
-
before: signatures[signatures.length-1].signature,
|
|
188
|
-
until: lastProcessedSignature.signature,
|
|
189
|
-
limit: this.logFetchLimit
|
|
190
|
-
}, "confirmed");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
signatures = signatures.concat(fetched);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return signatures;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Gets single latest known signature
|
|
201
|
-
*
|
|
202
|
-
* @private
|
|
203
|
-
*/
|
|
204
|
-
private async getFirstSignature(): Promise<ConfirmedSignatureInfo[]> {
|
|
205
|
-
return await this.connection.getSignaturesForAddress(this.solanaSwapProgram.program.programId, {
|
|
206
|
-
limit: 1
|
|
207
|
-
}, "confirmed");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Processes signatures, fetches transactions & processes event through event handlers
|
|
212
|
-
*
|
|
213
|
-
* @param signatures
|
|
214
|
-
* @private
|
|
215
|
-
* @returns {Promise<{signature: string, slot: number}>} latest processed transaction signature and slot height
|
|
216
|
-
*/
|
|
217
|
-
private async processSignatures(signatures: ConfirmedSignatureInfo[]): Promise<{signature: string, slot: number}> {
|
|
218
|
-
let lastSuccessfulSignature: {signature: string, slot: number} = null;
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
for(let i=signatures.length-1;i>=0;i--) {
|
|
222
|
-
const txSignature = signatures[i];
|
|
223
|
-
|
|
224
|
-
//Check if signature is already being processed by the
|
|
225
|
-
const signaturePromise = this.signaturesProcessing[txSignature.signature];
|
|
226
|
-
if(signaturePromise!=null) {
|
|
227
|
-
const result = await signaturePromise;
|
|
228
|
-
delete this.signaturesProcessing[txSignature.signature];
|
|
229
|
-
if(result) {
|
|
230
|
-
lastSuccessfulSignature = txSignature;
|
|
231
|
-
this.addProcessedSignature(txSignature.signature);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
this.logger.debug("processSignatures(): Process signature: ", txSignature);
|
|
237
|
-
|
|
238
|
-
const processPromise: Promise<boolean> = this.fetchTxAndProcessEvent(txSignature.signature);
|
|
239
|
-
this.signaturesProcessing[txSignature.signature] = processPromise;
|
|
240
|
-
|
|
241
|
-
const result = await processPromise;
|
|
242
|
-
if(!result) throw new Error("Failed to process signature: "+txSignature);
|
|
243
|
-
lastSuccessfulSignature = txSignature;
|
|
244
|
-
this.addProcessedSignature(txSignature.signature);
|
|
245
|
-
delete this.signaturesProcessing[txSignature.signature];
|
|
246
|
-
}
|
|
247
|
-
} catch (e) {
|
|
248
|
-
this.logger.error("processSignatures(): Failed processing signatures: ", e);
|
|
249
|
-
}
|
|
250
|
-
return lastSuccessfulSignature;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Polls for new events & processes them
|
|
255
|
-
*
|
|
256
|
-
* @private
|
|
257
|
-
*/
|
|
258
|
-
private async checkEvents() {
|
|
259
|
-
const lastSignature = await this.getLastSignature();
|
|
260
|
-
|
|
261
|
-
let signatures: ConfirmedSignatureInfo[] =
|
|
262
|
-
lastSignature==null ? await this.getFirstSignature() : await this.getNewSignatures(lastSignature);
|
|
263
|
-
|
|
264
|
-
let lastSuccessfulSignature: {signature: string, slot: number} = await this.processSignatures(signatures);
|
|
265
|
-
|
|
266
|
-
if(lastSuccessfulSignature!=null) {
|
|
267
|
-
await this.saveLastSignature(lastSuccessfulSignature.signature, lastSuccessfulSignature.slot);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async setupHttpPolling() {
|
|
272
|
-
this.stopped = false;
|
|
273
|
-
let func;
|
|
274
|
-
func = async () => {
|
|
275
|
-
await this.checkEvents().catch(e => {
|
|
276
|
-
this.logger.error("setupHttpPolling(): Failed to fetch Solana log: ", e);
|
|
277
|
-
});
|
|
278
|
-
if(this.stopped) return;
|
|
279
|
-
this.timeout = setTimeout(func, this.logFetchInterval);
|
|
280
|
-
};
|
|
281
|
-
await func();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async init(): Promise<void> {
|
|
285
|
-
try {
|
|
286
|
-
await fs.mkdir(this.directory);
|
|
287
|
-
} catch (e) {}
|
|
288
|
-
|
|
289
|
-
await this.setupHttpPolling();
|
|
290
|
-
this.setupWebsocket();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
stop(): Promise<void> {
|
|
294
|
-
this.stopped = true;
|
|
295
|
-
if(this.timeout!=null) clearTimeout(this.timeout)
|
|
296
|
-
return super.stop();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
}
|
|
1
|
+
import {ConfirmedSignatureInfo, Connection, ParsedTransactionWithMeta} from "@solana/web3.js";
|
|
2
|
+
import {IdlEvents} from "@coral-xyz/anchor";
|
|
3
|
+
import * as fs from "fs/promises";
|
|
4
|
+
import {SolanaSwapProgram} from "../swaps/SolanaSwapProgram";
|
|
5
|
+
import {EventObject, SolanaChainEventsBrowser} from "./SolanaChainEventsBrowser";
|
|
6
|
+
import {SwapProgram} from "../swaps/programTypes";
|
|
7
|
+
|
|
8
|
+
const BLOCKHEIGHT_FILENAME = "/blockheight.txt";
|
|
9
|
+
const LOG_FETCH_INTERVAL = 5*1000;
|
|
10
|
+
const LOG_FETCH_LIMIT = 500;
|
|
11
|
+
|
|
12
|
+
const PROCESSED_SIGNATURES_BACKLOG = 100;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Event handler for backend Node.js systems with access to fs, uses HTTP polling in combination with WS to not miss
|
|
16
|
+
* any events
|
|
17
|
+
*/
|
|
18
|
+
export class SolanaChainEvents extends SolanaChainEventsBrowser {
|
|
19
|
+
|
|
20
|
+
private readonly directory: string;
|
|
21
|
+
private readonly logFetchInterval: number;
|
|
22
|
+
private readonly logFetchLimit: number;
|
|
23
|
+
|
|
24
|
+
private signaturesProcessing: {
|
|
25
|
+
[signature: string]: Promise<boolean>
|
|
26
|
+
} = {};
|
|
27
|
+
private processedSignatures: string[] = [];
|
|
28
|
+
private processedSignaturesIndex: number = 0;
|
|
29
|
+
private stopped: boolean;
|
|
30
|
+
private timeout: NodeJS.Timeout;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
directory: string,
|
|
34
|
+
connection: Connection,
|
|
35
|
+
solanaSwapProgram: SolanaSwapProgram,
|
|
36
|
+
logFetchInterval?: number,
|
|
37
|
+
logFetchLimit?: number
|
|
38
|
+
) {
|
|
39
|
+
super(connection, solanaSwapProgram)
|
|
40
|
+
this.directory = directory;
|
|
41
|
+
this.logFetchInterval = logFetchInterval || LOG_FETCH_INTERVAL;
|
|
42
|
+
this.logFetchLimit = logFetchLimit || LOG_FETCH_LIMIT;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private addProcessedSignature(signature: string) {
|
|
46
|
+
this.processedSignatures[this.processedSignaturesIndex] = signature;
|
|
47
|
+
this.processedSignaturesIndex += 1;
|
|
48
|
+
if(this.processedSignaturesIndex >= PROCESSED_SIGNATURES_BACKLOG) this.processedSignaturesIndex = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private isSignatureProcessed(signature: string): boolean {
|
|
52
|
+
return this.processedSignatures.includes(signature);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Retrieves last signature & slot from filesystem
|
|
57
|
+
*
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
private async getLastSignature(): Promise<{
|
|
61
|
+
signature: string,
|
|
62
|
+
slot: number
|
|
63
|
+
}> {
|
|
64
|
+
try {
|
|
65
|
+
const txt = (await fs.readFile(this.directory+BLOCKHEIGHT_FILENAME)).toString();
|
|
66
|
+
const arr = txt.split(";");
|
|
67
|
+
if(arr.length<2) return {
|
|
68
|
+
signature: txt,
|
|
69
|
+
slot: 0
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
signature: arr[0],
|
|
73
|
+
slot: parseInt(arr[1])
|
|
74
|
+
};
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Saves last signature & slot to the filesystem
|
|
82
|
+
*
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
private saveLastSignature(lastSignature: string, slot: number): Promise<void> {
|
|
86
|
+
return fs.writeFile(this.directory+BLOCKHEIGHT_FILENAME, lastSignature+";"+slot);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses EventObject from the transaction
|
|
91
|
+
*
|
|
92
|
+
* @param transaction
|
|
93
|
+
* @private
|
|
94
|
+
* @returns {EventObject} parsed event object
|
|
95
|
+
*/
|
|
96
|
+
private getEventObjectFromTransaction(transaction: ParsedTransactionWithMeta): EventObject {
|
|
97
|
+
if(transaction.meta.err!=null) return null;
|
|
98
|
+
|
|
99
|
+
const instructions = this.solanaSwapProgram.Events.decodeInstructions(transaction.transaction.message);
|
|
100
|
+
const events = this.solanaSwapProgram.Events.parseLogs(transaction.meta.logMessages);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
instructions,
|
|
104
|
+
events,
|
|
105
|
+
blockTime: transaction.blockTime,
|
|
106
|
+
signature: transaction.transaction.signatures[0]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetches transaction from the RPC, parses it to even object & processes it through event handler
|
|
112
|
+
*
|
|
113
|
+
* @param signature
|
|
114
|
+
* @private
|
|
115
|
+
* @returns {boolean} whether the operation was successful
|
|
116
|
+
*/
|
|
117
|
+
private async fetchTxAndProcessEvent(signature: string): Promise<boolean> {
|
|
118
|
+
try {
|
|
119
|
+
const transaction = await this.connection.getParsedTransaction(signature, {
|
|
120
|
+
commitment: "confirmed",
|
|
121
|
+
maxSupportedTransactionVersion: 1
|
|
122
|
+
});
|
|
123
|
+
if(transaction==null) return false;
|
|
124
|
+
|
|
125
|
+
const eventObject = this.getEventObjectFromTransaction(transaction);
|
|
126
|
+
if(eventObject==null) return true;
|
|
127
|
+
|
|
128
|
+
await this.processEvent(eventObject);
|
|
129
|
+
return true;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
this.logger.error("fetchTxAndProcessEvent(): Error fetching transaction and processing event, signature: "+signature, e);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns websocket event handler for specific event type
|
|
138
|
+
*
|
|
139
|
+
* @param name
|
|
140
|
+
* @protected
|
|
141
|
+
* @returns event handler to be passed to program's addEventListener function
|
|
142
|
+
*/
|
|
143
|
+
protected getWsEventHandler<E extends "InitializeEvent" | "RefundEvent" | "ClaimEvent">(
|
|
144
|
+
name: E
|
|
145
|
+
): (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => void {
|
|
146
|
+
return (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => {
|
|
147
|
+
if(this.signaturesProcessing[signature]!=null) return;
|
|
148
|
+
if(this.isSignatureProcessed(signature)) return;
|
|
149
|
+
|
|
150
|
+
this.logger.debug("getWsEventHandler("+name+"): Process signature: ", signature);
|
|
151
|
+
|
|
152
|
+
this.signaturesProcessing[signature] = this.processEvent({
|
|
153
|
+
events: [{name, data: data as any}],
|
|
154
|
+
instructions: null, //Instructions will be fetched on-demand if required
|
|
155
|
+
blockTime: Math.floor(Date.now()/1000),
|
|
156
|
+
signature
|
|
157
|
+
}).then(() => true).catch(e => {
|
|
158
|
+
this.logger.error("getWsEventHandler("+name+"): Error processing signature: "+signature, e);
|
|
159
|
+
return false;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Gets all the new signatures from the last processed signature
|
|
166
|
+
*
|
|
167
|
+
* @param lastProcessedSignature
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
private async getNewSignatures(lastProcessedSignature: {signature: string, slot: number}): Promise<ConfirmedSignatureInfo[]> {
|
|
171
|
+
let signatures: ConfirmedSignatureInfo[] = [];
|
|
172
|
+
|
|
173
|
+
let fetched = null;
|
|
174
|
+
while(fetched==null || fetched.length===this.logFetchLimit) {
|
|
175
|
+
if(signatures.length===0) {
|
|
176
|
+
fetched = await this.connection.getSignaturesForAddress(this.solanaSwapProgram.program.programId, {
|
|
177
|
+
until: lastProcessedSignature.signature,
|
|
178
|
+
limit: this.logFetchLimit
|
|
179
|
+
}, "confirmed");
|
|
180
|
+
//Check if newest returned signature (index 0) is older than the latest signature's slot, this is a sanity check
|
|
181
|
+
if(fetched.length>0 && fetched[0].slot<lastProcessedSignature.slot) {
|
|
182
|
+
this.logger.debug("getNewSignatures(): Sanity check triggered, returned signature slot height is older than latest!");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
fetched = await this.connection.getSignaturesForAddress(this.solanaSwapProgram.program.programId, {
|
|
187
|
+
before: signatures[signatures.length-1].signature,
|
|
188
|
+
until: lastProcessedSignature.signature,
|
|
189
|
+
limit: this.logFetchLimit
|
|
190
|
+
}, "confirmed");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
signatures = signatures.concat(fetched);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return signatures;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Gets single latest known signature
|
|
201
|
+
*
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
private async getFirstSignature(): Promise<ConfirmedSignatureInfo[]> {
|
|
205
|
+
return await this.connection.getSignaturesForAddress(this.solanaSwapProgram.program.programId, {
|
|
206
|
+
limit: 1
|
|
207
|
+
}, "confirmed");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Processes signatures, fetches transactions & processes event through event handlers
|
|
212
|
+
*
|
|
213
|
+
* @param signatures
|
|
214
|
+
* @private
|
|
215
|
+
* @returns {Promise<{signature: string, slot: number}>} latest processed transaction signature and slot height
|
|
216
|
+
*/
|
|
217
|
+
private async processSignatures(signatures: ConfirmedSignatureInfo[]): Promise<{signature: string, slot: number}> {
|
|
218
|
+
let lastSuccessfulSignature: {signature: string, slot: number} = null;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
for(let i=signatures.length-1;i>=0;i--) {
|
|
222
|
+
const txSignature = signatures[i];
|
|
223
|
+
|
|
224
|
+
//Check if signature is already being processed by the
|
|
225
|
+
const signaturePromise = this.signaturesProcessing[txSignature.signature];
|
|
226
|
+
if(signaturePromise!=null) {
|
|
227
|
+
const result = await signaturePromise;
|
|
228
|
+
delete this.signaturesProcessing[txSignature.signature];
|
|
229
|
+
if(result) {
|
|
230
|
+
lastSuccessfulSignature = txSignature;
|
|
231
|
+
this.addProcessedSignature(txSignature.signature);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.logger.debug("processSignatures(): Process signature: ", txSignature);
|
|
237
|
+
|
|
238
|
+
const processPromise: Promise<boolean> = this.fetchTxAndProcessEvent(txSignature.signature);
|
|
239
|
+
this.signaturesProcessing[txSignature.signature] = processPromise;
|
|
240
|
+
|
|
241
|
+
const result = await processPromise;
|
|
242
|
+
if(!result) throw new Error("Failed to process signature: "+txSignature);
|
|
243
|
+
lastSuccessfulSignature = txSignature;
|
|
244
|
+
this.addProcessedSignature(txSignature.signature);
|
|
245
|
+
delete this.signaturesProcessing[txSignature.signature];
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
this.logger.error("processSignatures(): Failed processing signatures: ", e);
|
|
249
|
+
}
|
|
250
|
+
return lastSuccessfulSignature;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Polls for new events & processes them
|
|
255
|
+
*
|
|
256
|
+
* @private
|
|
257
|
+
*/
|
|
258
|
+
private async checkEvents() {
|
|
259
|
+
const lastSignature = await this.getLastSignature();
|
|
260
|
+
|
|
261
|
+
let signatures: ConfirmedSignatureInfo[] =
|
|
262
|
+
lastSignature==null ? await this.getFirstSignature() : await this.getNewSignatures(lastSignature);
|
|
263
|
+
|
|
264
|
+
let lastSuccessfulSignature: {signature: string, slot: number} = await this.processSignatures(signatures);
|
|
265
|
+
|
|
266
|
+
if(lastSuccessfulSignature!=null) {
|
|
267
|
+
await this.saveLastSignature(lastSuccessfulSignature.signature, lastSuccessfulSignature.slot);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async setupHttpPolling() {
|
|
272
|
+
this.stopped = false;
|
|
273
|
+
let func;
|
|
274
|
+
func = async () => {
|
|
275
|
+
await this.checkEvents().catch(e => {
|
|
276
|
+
this.logger.error("setupHttpPolling(): Failed to fetch Solana log: ", e);
|
|
277
|
+
});
|
|
278
|
+
if(this.stopped) return;
|
|
279
|
+
this.timeout = setTimeout(func, this.logFetchInterval);
|
|
280
|
+
};
|
|
281
|
+
await func();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async init(): Promise<void> {
|
|
285
|
+
try {
|
|
286
|
+
await fs.mkdir(this.directory);
|
|
287
|
+
} catch (e) {}
|
|
288
|
+
|
|
289
|
+
await this.setupHttpPolling();
|
|
290
|
+
this.setupWebsocket();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
stop(): Promise<void> {
|
|
294
|
+
this.stopped = true;
|
|
295
|
+
if(this.timeout!=null) clearTimeout(this.timeout)
|
|
296
|
+
return super.stop();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
}
|