@atomiqlabs/chain-solana 13.5.13 → 13.5.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/README.md +73 -73
- package/dist/index.d.ts +81 -81
- package/dist/index.js +102 -102
- package/dist/node/index.d.ts +9 -9
- package/dist/node/index.js +13 -13
- package/dist/solana/SolanaChainType.d.ts +15 -15
- package/dist/solana/SolanaChainType.js +2 -2
- package/dist/solana/SolanaChains.d.ts +12 -12
- package/dist/solana/SolanaChains.js +45 -45
- package/dist/solana/SolanaInitializer.d.ts +94 -94
- package/dist/solana/SolanaInitializer.js +174 -174
- package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +222 -222
- package/dist/solana/btcrelay/SolanaBtcRelay.js +455 -455
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +84 -84
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +70 -70
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +92 -92
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +109 -109
- package/dist/solana/btcrelay/program/programIdl.json +671 -671
- package/dist/solana/chain/SolanaAction.d.ts +26 -26
- package/dist/solana/chain/SolanaAction.js +87 -87
- package/dist/solana/chain/SolanaChainInterface.d.ts +224 -224
- package/dist/solana/chain/SolanaChainInterface.js +275 -275
- 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 +32 -32
- package/dist/solana/chain/modules/SolanaBlocks.js +78 -78
- package/dist/solana/chain/modules/SolanaEvents.d.ts +68 -68
- package/dist/solana/chain/modules/SolanaEvents.js +238 -238
- package/dist/solana/chain/modules/SolanaFees.d.ts +189 -189
- package/dist/solana/chain/modules/SolanaFees.js +434 -434
- package/dist/solana/chain/modules/SolanaSignatures.d.ts +24 -24
- package/dist/solana/chain/modules/SolanaSignatures.js +39 -39
- package/dist/solana/chain/modules/SolanaSlots.d.ts +33 -33
- package/dist/solana/chain/modules/SolanaSlots.js +72 -72
- package/dist/solana/chain/modules/SolanaTokens.d.ts +123 -123
- package/dist/solana/chain/modules/SolanaTokens.js +242 -242
- package/dist/solana/chain/modules/SolanaTransactions.d.ts +149 -149
- package/dist/solana/chain/modules/SolanaTransactions.js +445 -445
- package/dist/solana/connection/ConnectionWithRetries.d.ts +35 -35
- package/dist/solana/connection/ConnectionWithRetries.js +86 -71
- package/dist/solana/events/SolanaChainEvents.d.ts +45 -45
- package/dist/solana/events/SolanaChainEvents.js +108 -108
- package/dist/solana/events/SolanaChainEventsBrowser.d.ts +205 -205
- package/dist/solana/events/SolanaChainEventsBrowser.js +404 -404
- package/dist/solana/program/SolanaProgramBase.d.ts +73 -73
- package/dist/solana/program/SolanaProgramBase.js +54 -54
- 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 +117 -117
- package/dist/solana/swaps/SolanaSwapData.d.ts +333 -333
- package/dist/solana/swaps/SolanaSwapData.js +535 -535
- package/dist/solana/swaps/SolanaSwapModule.d.ts +11 -11
- package/dist/solana/swaps/SolanaSwapModule.js +12 -12
- package/dist/solana/swaps/SolanaSwapProgram.d.ts +376 -376
- package/dist/solana/swaps/SolanaSwapProgram.js +769 -769
- package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -11
- package/dist/solana/swaps/SwapTypeEnum.js +43 -43
- package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +95 -95
- package/dist/solana/swaps/modules/SolanaDataAccount.js +232 -232
- package/dist/solana/swaps/modules/SolanaLpVault.d.ts +69 -69
- package/dist/solana/swaps/modules/SolanaLpVault.js +171 -171
- package/dist/solana/swaps/modules/SwapClaim.d.ts +126 -126
- package/dist/solana/swaps/modules/SwapClaim.js +294 -294
- package/dist/solana/swaps/modules/SwapInit.d.ts +213 -213
- package/dist/solana/swaps/modules/SwapInit.js +658 -658
- package/dist/solana/swaps/modules/SwapRefund.d.ts +87 -87
- package/dist/solana/swaps/modules/SwapRefund.js +293 -293
- 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/swaps/v1/programIdl.json +945 -945
- package/dist/solana/swaps/v1/programTypes.d.ts +943 -943
- package/dist/solana/swaps/v1/programTypes.js +945 -945
- package/dist/solana/swaps/v2/programIdl.json +952 -952
- package/dist/solana/swaps/v2/programTypes.d.ts +950 -950
- package/dist/solana/swaps/v2/programTypes.js +952 -952
- package/dist/solana/wallet/SolanaKeypairWallet.d.ts +29 -29
- package/dist/solana/wallet/SolanaKeypairWallet.js +50 -50
- package/dist/solana/wallet/SolanaSigner.d.ts +30 -30
- package/dist/solana/wallet/SolanaSigner.js +30 -30
- package/dist/utils/Utils.d.ts +58 -58
- package/dist/utils/Utils.js +170 -170
- package/node/index.d.ts +1 -1
- package/node/index.js +3 -3
- package/package.json +46 -46
- package/src/index.ts +87 -87
- package/src/node/index.ts +9 -9
- package/src/solana/SolanaChainType.ts +32 -32
- package/src/solana/SolanaChains.ts +46 -46
- package/src/solana/SolanaInitializer.ts +278 -278
- package/src/solana/btcrelay/SolanaBtcRelay.ts +615 -615
- package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +116 -116
- package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +148 -148
- package/src/solana/btcrelay/program/programIdl.json +670 -670
- package/src/solana/chain/SolanaAction.ts +109 -109
- package/src/solana/chain/SolanaChainInterface.ts +404 -404
- package/src/solana/chain/SolanaModule.ts +20 -20
- package/src/solana/chain/modules/SolanaAddresses.ts +20 -20
- package/src/solana/chain/modules/SolanaBlocks.ts +89 -89
- package/src/solana/chain/modules/SolanaEvents.ts +271 -271
- package/src/solana/chain/modules/SolanaFees.ts +522 -522
- package/src/solana/chain/modules/SolanaSignatures.ts +39 -39
- package/src/solana/chain/modules/SolanaSlots.ts +85 -85
- package/src/solana/chain/modules/SolanaTokens.ts +300 -300
- package/src/solana/chain/modules/SolanaTransactions.ts +503 -503
- package/src/solana/connection/ConnectionWithRetries.ts +113 -96
- package/src/solana/events/SolanaChainEvents.ts +127 -127
- package/src/solana/events/SolanaChainEventsBrowser.ts +495 -495
- package/src/solana/program/SolanaProgramBase.ts +119 -119
- package/src/solana/program/SolanaProgramModule.ts +15 -15
- package/src/solana/program/modules/SolanaProgramEvents.ts +157 -157
- package/src/solana/swaps/SolanaSwapData.ts +735 -735
- package/src/solana/swaps/SolanaSwapModule.ts +19 -19
- package/src/solana/swaps/SolanaSwapProgram.ts +1074 -1074
- package/src/solana/swaps/SwapTypeEnum.ts +30 -30
- package/src/solana/swaps/modules/SolanaDataAccount.ts +302 -302
- package/src/solana/swaps/modules/SolanaLpVault.ts +208 -208
- package/src/solana/swaps/modules/SwapClaim.ts +387 -387
- package/src/solana/swaps/modules/SwapInit.ts +785 -785
- package/src/solana/swaps/modules/SwapRefund.ts +353 -353
- package/src/solana/swaps/v1/programIdl.json +944 -944
- package/src/solana/swaps/v1/programTypes.ts +1885 -1885
- package/src/solana/swaps/v2/programIdl.json +951 -951
- package/src/solana/swaps/v2/programTypes.ts +1899 -1899
- package/src/solana/wallet/SolanaKeypairWallet.ts +56 -56
- package/src/solana/wallet/SolanaSigner.ts +43 -43
- package/src/utils/Utils.ts +194 -194
|
@@ -1,495 +1,495 @@
|
|
|
1
|
-
import {ChainEvents, ClaimEvent, EventListener, InitializeEvent, RefundEvent, SwapEvent} from "@atomiqlabs/base";
|
|
2
|
-
import {InitInstruction, SolanaSwapData} from "../swaps/SolanaSwapData";
|
|
3
|
-
import {IdlEvents} from "@coral-xyz/anchor";
|
|
4
|
-
import {SolanaSwapProgram} from "../swaps/SolanaSwapProgram";
|
|
5
|
-
import {
|
|
6
|
-
getLogger,
|
|
7
|
-
onceAsync, toEscrowHash, tryWithRetries
|
|
8
|
-
} from "../../utils/Utils";
|
|
9
|
-
import {ConfirmedSignatureInfo, Connection, ParsedTransactionWithMeta} from "@solana/web3.js";
|
|
10
|
-
import {SwapTypeEnum} from "../swaps/SwapTypeEnum";
|
|
11
|
-
import {
|
|
12
|
-
InstructionWithAccounts,
|
|
13
|
-
ProgramEvent
|
|
14
|
-
} from "../program/modules/SolanaProgramEvents";
|
|
15
|
-
import {SwapProgram} from "../swaps/v1/programTypes";
|
|
16
|
-
import {Buffer} from "buffer";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parsed event payload grouped by originating transaction metadata.
|
|
20
|
-
*
|
|
21
|
-
* @category Events
|
|
22
|
-
*/
|
|
23
|
-
export type EventObject = {
|
|
24
|
-
events: ProgramEvent<SwapProgram>[],
|
|
25
|
-
instructions?: (InstructionWithAccounts<SwapProgram> | null)[],
|
|
26
|
-
blockTime: number,
|
|
27
|
-
signature: string
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const LOG_FETCH_LIMIT = 500;
|
|
31
|
-
const PROCESSED_SIGNATURES_BACKLOG = 500;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Legacy current cursor of Solana event listener state.
|
|
35
|
-
*
|
|
36
|
-
* @category Events
|
|
37
|
-
*/
|
|
38
|
-
export type SolanaLegacyEventListenerState = {
|
|
39
|
-
/**
|
|
40
|
-
* Last processed transaction's signature
|
|
41
|
-
*/
|
|
42
|
-
signature: string,
|
|
43
|
-
/**
|
|
44
|
-
* Last processed transaction's slot
|
|
45
|
-
*/
|
|
46
|
-
slot: number
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Current cursor of Solana event listener state.
|
|
51
|
-
*
|
|
52
|
-
* @category Events
|
|
53
|
-
*/
|
|
54
|
-
export type SolanaEventListenerState = {
|
|
55
|
-
[version: string]: SolanaLegacyEventListenerState | null
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
function toNewEventListenerState(obj: SolanaLegacyEventListenerState | SolanaEventListenerState | undefined): SolanaEventListenerState | undefined {
|
|
59
|
-
if(obj==null) return undefined;
|
|
60
|
-
if(obj.slot!=null || obj.signature!=null) return {"v1": obj} as SolanaEventListenerState;
|
|
61
|
-
return obj as SolanaEventListenerState;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Solana on-chain event handler for front-end systems without access to fs, uses pure WS to subscribe, might lose
|
|
66
|
-
* out on some events if the network is unreliable, front-end systems should take this into consideration and not
|
|
67
|
-
* rely purely on events
|
|
68
|
-
*
|
|
69
|
-
* @category Events
|
|
70
|
-
*/
|
|
71
|
-
export class SolanaChainEventsBrowser implements ChainEvents<SolanaSwapData, SolanaLegacyEventListenerState | SolanaEventListenerState> {
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @internal
|
|
75
|
-
*/
|
|
76
|
-
protected readonly listeners: EventListener<SolanaSwapData>[] = [];
|
|
77
|
-
/**
|
|
78
|
-
* @internal
|
|
79
|
-
*/
|
|
80
|
-
protected readonly connection: Connection;
|
|
81
|
-
/**
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
protected readonly contractVersions: {[version: string]: {swapContract: SolanaSwapProgram}};
|
|
85
|
-
/**
|
|
86
|
-
* @internal
|
|
87
|
-
*/
|
|
88
|
-
protected eventListeners: {[version: string]: number[]} = {};
|
|
89
|
-
/**
|
|
90
|
-
* @internal
|
|
91
|
-
*/
|
|
92
|
-
protected readonly logger = getLogger("SolanaChainEventsBrowser: ");
|
|
93
|
-
|
|
94
|
-
private readonly logFetchLimit: number;
|
|
95
|
-
private signaturesProcessing: {
|
|
96
|
-
[signature: string]: Promise<boolean>
|
|
97
|
-
} = {};
|
|
98
|
-
private processedSignatures: string[] = [];
|
|
99
|
-
private processedSignaturesIndex: number = 0;
|
|
100
|
-
|
|
101
|
-
constructor(
|
|
102
|
-
connection: Connection,
|
|
103
|
-
contractVersions: SolanaSwapProgram | {[version: string]: {swapContract: SolanaSwapProgram}},
|
|
104
|
-
logFetchLimit?: number
|
|
105
|
-
) {
|
|
106
|
-
this.connection = connection;
|
|
107
|
-
if(contractVersions instanceof SolanaSwapProgram) {
|
|
108
|
-
this.contractVersions = {[contractVersions.version]: {swapContract: contractVersions}};
|
|
109
|
-
} else {
|
|
110
|
-
this.contractVersions = contractVersions;
|
|
111
|
-
}
|
|
112
|
-
this.logFetchLimit = logFetchLimit ?? LOG_FETCH_LIMIT;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private addProcessedSignature(signature: string, version: string) {
|
|
116
|
-
this.processedSignatures[this.processedSignaturesIndex] = signature+"-"+version;
|
|
117
|
-
this.processedSignaturesIndex += 1;
|
|
118
|
-
if(this.processedSignaturesIndex >= PROCESSED_SIGNATURES_BACKLOG) this.processedSignaturesIndex = 0;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private isSignatureProcessed(signature: string, version: string): boolean {
|
|
122
|
-
return this.processedSignatures.includes(signature+"-"+version);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Parses EventObject from the transaction
|
|
127
|
-
*
|
|
128
|
-
* @param transaction
|
|
129
|
-
* @param version
|
|
130
|
-
* @private
|
|
131
|
-
* @returns {EventObject} parsed event object
|
|
132
|
-
*/
|
|
133
|
-
private getEventObjectFromTransaction(transaction: ParsedTransactionWithMeta, version: string): EventObject | null {
|
|
134
|
-
const signature = transaction.transaction.signatures[0];
|
|
135
|
-
if(transaction.meta==null) throw new Error(`Transaction 'meta' not found for Solana tx: ${signature}`);
|
|
136
|
-
if(transaction.meta.err!=null || transaction.meta.logMessages==null) return null;
|
|
137
|
-
|
|
138
|
-
const instructions = this.contractVersions[version].swapContract._Events.decodeInstructions(transaction.transaction.message);
|
|
139
|
-
const events = this.contractVersions[version].swapContract._Events.parseLogs(transaction.meta.logMessages);
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
instructions,
|
|
143
|
-
events,
|
|
144
|
-
blockTime: transaction.blockTime!,
|
|
145
|
-
signature: signature
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Fetches transaction from the RPC, parses it to even object & processes it through event handler
|
|
151
|
-
*
|
|
152
|
-
* @param signature
|
|
153
|
-
* @param version
|
|
154
|
-
* @private
|
|
155
|
-
* @returns {boolean} whether the operation was successful
|
|
156
|
-
*/
|
|
157
|
-
private async fetchTxAndProcessEvent(signature: string, version: string): Promise<boolean> {
|
|
158
|
-
try {
|
|
159
|
-
const transaction = await this.connection.getParsedTransaction(signature, {
|
|
160
|
-
commitment: "confirmed",
|
|
161
|
-
maxSupportedTransactionVersion: 1
|
|
162
|
-
});
|
|
163
|
-
if(transaction==null) return false;
|
|
164
|
-
|
|
165
|
-
const eventObject = this.getEventObjectFromTransaction(transaction, version);
|
|
166
|
-
if(eventObject==null) return true;
|
|
167
|
-
|
|
168
|
-
await this.processEvent(eventObject, version);
|
|
169
|
-
return true;
|
|
170
|
-
} catch (e) {
|
|
171
|
-
this.logger.error("fetchTxAndProcessEvent(): Error fetching transaction and processing event, signature: "+signature, e);
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Fetches and parses transaction instructions
|
|
178
|
-
*
|
|
179
|
-
* @private
|
|
180
|
-
* @returns {Promise<(InstructionWithAccounts<SwapProgram> | null)[] | null>} array of parsed instructions
|
|
181
|
-
*/
|
|
182
|
-
private async getTransactionInstructions(signature: string, version: string): Promise<(InstructionWithAccounts<SwapProgram> | null)[] | null> {
|
|
183
|
-
const transaction = await tryWithRetries<ParsedTransactionWithMeta>(async () => {
|
|
184
|
-
const res = await this.connection.getParsedTransaction(signature, {
|
|
185
|
-
commitment: "confirmed",
|
|
186
|
-
maxSupportedTransactionVersion: 0
|
|
187
|
-
});
|
|
188
|
-
if(res==null) throw new Error("Transaction not found!");
|
|
189
|
-
return res;
|
|
190
|
-
});
|
|
191
|
-
if(transaction.meta==null) throw new Error("Transaction 'meta' not found!");
|
|
192
|
-
if(transaction.meta.err!=null) return null;
|
|
193
|
-
return this.contractVersions[version].swapContract._Events.decodeInstructions(transaction.transaction.message);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Returns async getter for fetching on-demand initialize event swap data
|
|
198
|
-
*
|
|
199
|
-
* @param eventObject
|
|
200
|
-
* @param txoHash
|
|
201
|
-
* @param version
|
|
202
|
-
* @private
|
|
203
|
-
* @returns {() => Promise<SolanaSwapData>} getter to be passed to InitializeEvent constructor
|
|
204
|
-
*/
|
|
205
|
-
private getSwapDataGetter(eventObject: EventObject, txoHash: string, version: string): () => Promise<SolanaSwapData | null> {
|
|
206
|
-
return async () => {
|
|
207
|
-
if(eventObject.instructions==null) {
|
|
208
|
-
const ixs = await this.getTransactionInstructions(eventObject.signature, version);
|
|
209
|
-
if(ixs==null) return null;
|
|
210
|
-
eventObject.instructions = ixs;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const initIx = eventObject.instructions.find(
|
|
214
|
-
ix => ix!=null && (ix.name === "offererInitializePayIn" || ix.name === "offererInitialize")
|
|
215
|
-
) as InitInstruction;
|
|
216
|
-
if(initIx == null) return null;
|
|
217
|
-
|
|
218
|
-
return SolanaSwapData.fromInstruction(this.contractVersions[version].swapContract.program.programId, version as "v1" | "v2", initIx, txoHash);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* @internal
|
|
224
|
-
*/
|
|
225
|
-
protected parseInitializeEvent(data: IdlEvents<SwapProgram>["InitializeEvent"], eventObject: EventObject, version: string): InitializeEvent<SolanaSwapData> {
|
|
226
|
-
const paymentHash: string = Buffer.from(data.hash).toString("hex");
|
|
227
|
-
const txoHash: string = Buffer.from(data.txoHash).toString("hex");
|
|
228
|
-
const escrowHash = toEscrowHash(paymentHash, data.sequence);
|
|
229
|
-
this.logger.debug("InitializeEvent paymentHash: "+paymentHash+" sequence: "+data.sequence.toString(10)+
|
|
230
|
-
" txoHash: "+txoHash+" escrowHash: "+escrowHash);
|
|
231
|
-
return new InitializeEvent<SolanaSwapData>(
|
|
232
|
-
escrowHash,
|
|
233
|
-
SwapTypeEnum.toChainSwapType(data.kind),
|
|
234
|
-
onceAsync<SolanaSwapData | null>(this.getSwapDataGetter(eventObject, txoHash, version)),
|
|
235
|
-
version
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* @internal
|
|
241
|
-
*/
|
|
242
|
-
protected parseRefundEvent(data: IdlEvents<SwapProgram>["RefundEvent"], version: string): RefundEvent<SolanaSwapData> {
|
|
243
|
-
const paymentHash: string = Buffer.from(data.hash).toString("hex");
|
|
244
|
-
const escrowHash = toEscrowHash(paymentHash, data.sequence);
|
|
245
|
-
this.logger.debug("RefundEvent paymentHash: "+paymentHash+" sequence: "+data.sequence.toString(10)+
|
|
246
|
-
" escrowHash: "+escrowHash);
|
|
247
|
-
return new RefundEvent<SolanaSwapData>(escrowHash, version);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* @internal
|
|
252
|
-
*/
|
|
253
|
-
protected parseClaimEvent(data: IdlEvents<SwapProgram>["ClaimEvent"], version: string): ClaimEvent<SolanaSwapData> {
|
|
254
|
-
const secret: string = Buffer.from(data.secret).toString("hex");
|
|
255
|
-
const paymentHash: string = Buffer.from(data.hash).toString("hex");
|
|
256
|
-
const escrowHash = toEscrowHash(paymentHash, data.sequence);
|
|
257
|
-
this.logger.debug("ClaimEvent paymentHash: "+paymentHash+" sequence: "+data.sequence.toString(10)+
|
|
258
|
-
" secret: "+secret+" escrowHash: "+escrowHash);
|
|
259
|
-
return new ClaimEvent<SolanaSwapData>(escrowHash, secret, version);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Processes event as received from the chain, parses it & calls event listeners
|
|
264
|
-
*
|
|
265
|
-
* @param eventObject
|
|
266
|
-
* @param version
|
|
267
|
-
* @internal
|
|
268
|
-
*/
|
|
269
|
-
protected async processEvent(eventObject : EventObject, version: string) {
|
|
270
|
-
let parsedEvents: SwapEvent<SolanaSwapData>[] = eventObject.events.map(event => {
|
|
271
|
-
let parsedEvent: SwapEvent<SolanaSwapData> | undefined;
|
|
272
|
-
switch(event.name) {
|
|
273
|
-
case "ClaimEvent":
|
|
274
|
-
parsedEvent = this.parseClaimEvent(event.data, version);
|
|
275
|
-
break;
|
|
276
|
-
case "RefundEvent":
|
|
277
|
-
parsedEvent = this.parseRefundEvent(event.data, version);
|
|
278
|
-
break;
|
|
279
|
-
case "InitializeEvent":
|
|
280
|
-
parsedEvent = this.parseInitializeEvent(event.data, eventObject, version);
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
if(parsedEvent==null) return null;
|
|
284
|
-
(parsedEvent as any).meta = {
|
|
285
|
-
blockTime: eventObject.blockTime,
|
|
286
|
-
timestamp: eventObject.blockTime,
|
|
287
|
-
txId: eventObject.signature
|
|
288
|
-
};
|
|
289
|
-
return parsedEvent;
|
|
290
|
-
}).filter(parsedEvent => parsedEvent!=null);
|
|
291
|
-
|
|
292
|
-
for(let listener of this.listeners) {
|
|
293
|
-
await listener(parsedEvents);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Returns websocket event handler for specific event type
|
|
299
|
-
*
|
|
300
|
-
* @param name
|
|
301
|
-
* @param version
|
|
302
|
-
* @internal
|
|
303
|
-
* @returns event handler to be passed to program's addEventListener function
|
|
304
|
-
*/
|
|
305
|
-
protected getWsEventHandler<E extends "InitializeEvent" | "RefundEvent" | "ClaimEvent">(
|
|
306
|
-
name: E,
|
|
307
|
-
version: string
|
|
308
|
-
): (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => void {
|
|
309
|
-
return (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => {
|
|
310
|
-
if(this.signaturesProcessing[signature+"-"+version]!=null) return;
|
|
311
|
-
if(this.isSignatureProcessed(signature, version)) return;
|
|
312
|
-
|
|
313
|
-
this.logger.debug("getWsEventHandler("+name+"): Process signature: ", signature);
|
|
314
|
-
|
|
315
|
-
this.signaturesProcessing[signature+"-"+version] = this.processEvent({
|
|
316
|
-
events: [{name, data: data as any}],
|
|
317
|
-
blockTime: Math.floor(Date.now()/1000),
|
|
318
|
-
signature
|
|
319
|
-
}, version).then(() => true).catch(e => {
|
|
320
|
-
this.logger.error("getWsEventHandler("+name+"): Error processing signature: "+signature, e);
|
|
321
|
-
return false;
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Sets up event handlers listening for swap events over websocket
|
|
328
|
-
*
|
|
329
|
-
* @internal
|
|
330
|
-
*/
|
|
331
|
-
protected setupWebsocket() {
|
|
332
|
-
for(let version in this.contractVersions) {
|
|
333
|
-
const program = this.contractVersions[version].swapContract.program;
|
|
334
|
-
const eventListeners = this.eventListeners[version] ??= [];
|
|
335
|
-
eventListeners.push(program.addEventListener<"InitializeEvent">("InitializeEvent", this.getWsEventHandler("InitializeEvent", version)));
|
|
336
|
-
eventListeners.push(program.addEventListener<"ClaimEvent">("ClaimEvent", this.getWsEventHandler("ClaimEvent", version)));
|
|
337
|
-
eventListeners.push(program.addEventListener<"RefundEvent">("RefundEvent", this.getWsEventHandler("RefundEvent", version)));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Gets all the new signatures from the last processed signature
|
|
343
|
-
*
|
|
344
|
-
* @param lastProcessedSignature
|
|
345
|
-
* @param version
|
|
346
|
-
* @private
|
|
347
|
-
*/
|
|
348
|
-
private async getNewSignatures(lastProcessedSignature: {signature: string, slot: number}, version: string): Promise<ConfirmedSignatureInfo[] | null> {
|
|
349
|
-
let signatures: ConfirmedSignatureInfo[] = [];
|
|
350
|
-
|
|
351
|
-
let fetched = null;
|
|
352
|
-
while(fetched==null || fetched.length===this.logFetchLimit) {
|
|
353
|
-
if(signatures.length===0) {
|
|
354
|
-
fetched = await this.connection.getSignaturesForAddress(this.contractVersions[version].swapContract.program.programId, {
|
|
355
|
-
until: lastProcessedSignature.signature,
|
|
356
|
-
limit: this.logFetchLimit
|
|
357
|
-
}, "confirmed");
|
|
358
|
-
//Check if newest returned signature (index 0) is older than the latest signature's slot, this is a sanity check
|
|
359
|
-
if(fetched.length>0 && fetched[0].slot<lastProcessedSignature.slot) {
|
|
360
|
-
this.logger.debug("getNewSignatures(): Sanity check triggered, returned signature slot height is older than latest!");
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
fetched = await this.connection.getSignaturesForAddress(this.contractVersions[version].swapContract.program.programId, {
|
|
365
|
-
before: signatures[signatures.length-1].signature,
|
|
366
|
-
until: lastProcessedSignature.signature,
|
|
367
|
-
limit: this.logFetchLimit
|
|
368
|
-
}, "confirmed");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
signatures = signatures.concat(fetched);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return signatures;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Gets single latest known signature
|
|
379
|
-
*
|
|
380
|
-
* @private
|
|
381
|
-
*/
|
|
382
|
-
private async getFirstSignature(version: string): Promise<ConfirmedSignatureInfo[]> {
|
|
383
|
-
return await this.connection.getSignaturesForAddress(this.contractVersions[version].swapContract.program.programId, {
|
|
384
|
-
limit: 1
|
|
385
|
-
}, "confirmed");
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Processes signatures, fetches transactions & processes event through event handlers
|
|
390
|
-
*
|
|
391
|
-
* @param signatures
|
|
392
|
-
* @param version
|
|
393
|
-
* @private
|
|
394
|
-
* @returns {Promise<{signature: string, slot: number}>} latest processed transaction signature and slot height
|
|
395
|
-
*/
|
|
396
|
-
private async processSignatures(signatures: ConfirmedSignatureInfo[], version: string): Promise<{signature: string, slot: number} | null> {
|
|
397
|
-
let lastSuccessfulSignature: {signature: string, slot: number} | null = null;
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
for(let i=signatures.length-1;i>=0;i--) {
|
|
401
|
-
const txSignature = signatures[i];
|
|
402
|
-
|
|
403
|
-
//Check if signature is already being processed by the
|
|
404
|
-
const signaturePromise = this.signaturesProcessing[txSignature.signature+"-"+version];
|
|
405
|
-
if(signaturePromise!=null) {
|
|
406
|
-
const result = await signaturePromise;
|
|
407
|
-
delete this.signaturesProcessing[txSignature.signature+"-"+version];
|
|
408
|
-
if(result) {
|
|
409
|
-
lastSuccessfulSignature = txSignature;
|
|
410
|
-
this.addProcessedSignature(txSignature.signature, version);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
this.logger.debug("processSignatures(): Process signature: ", txSignature);
|
|
416
|
-
|
|
417
|
-
const processPromise: Promise<boolean> = this.fetchTxAndProcessEvent(txSignature.signature, version);
|
|
418
|
-
this.signaturesProcessing[txSignature.signature+"-"+version] = processPromise;
|
|
419
|
-
|
|
420
|
-
const result = await processPromise;
|
|
421
|
-
if(!result) throw new Error("Failed to process signature: "+txSignature);
|
|
422
|
-
lastSuccessfulSignature = txSignature;
|
|
423
|
-
this.addProcessedSignature(txSignature.signature, version);
|
|
424
|
-
delete this.signaturesProcessing[txSignature.signature+"-"+version];
|
|
425
|
-
}
|
|
426
|
-
} catch (e) {
|
|
427
|
-
this.logger.error("processSignatures(): Failed processing signatures: ", e);
|
|
428
|
-
}
|
|
429
|
-
return lastSuccessfulSignature;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* @inheritDoc
|
|
434
|
-
*/
|
|
435
|
-
async poll(lastSignature?: SolanaLegacyEventListenerState | SolanaEventListenerState): Promise<SolanaEventListenerState> {
|
|
436
|
-
const _lastSignature = toNewEventListenerState(lastSignature);
|
|
437
|
-
|
|
438
|
-
const result: SolanaEventListenerState = {};
|
|
439
|
-
for(let version in this.contractVersions) {
|
|
440
|
-
const lastSignature = _lastSignature?.[version];
|
|
441
|
-
|
|
442
|
-
let signatures = lastSignature==null
|
|
443
|
-
? await this.getFirstSignature(version)
|
|
444
|
-
: await this.getNewSignatures(lastSignature, version);
|
|
445
|
-
if(signatures==null) {
|
|
446
|
-
result[version] = lastSignature ?? null;
|
|
447
|
-
} else {
|
|
448
|
-
let lastSuccessfulSignature = await this.processSignatures(signatures, version);
|
|
449
|
-
result[version] = lastSuccessfulSignature ?? lastSignature ?? null;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return result;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* @inheritDoc
|
|
458
|
-
*/
|
|
459
|
-
init(noAutomaticPoll?: boolean): Promise<void> {
|
|
460
|
-
if(noAutomaticPoll) return Promise.resolve();
|
|
461
|
-
this.setupWebsocket();
|
|
462
|
-
return Promise.resolve();
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* @inheritDoc
|
|
467
|
-
*/
|
|
468
|
-
async stop(): Promise<void> {
|
|
469
|
-
for(let version in this.eventListeners) {
|
|
470
|
-
for(let num of this.eventListeners[version]) {
|
|
471
|
-
await this.contractVersions[version].swapContract.program.removeEventListener(num);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
this.eventListeners = {};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* @inheritDoc
|
|
479
|
-
*/
|
|
480
|
-
registerListener(cbk: EventListener<SolanaSwapData>): void {
|
|
481
|
-
this.listeners.push(cbk);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* @inheritDoc
|
|
486
|
-
*/
|
|
487
|
-
unregisterListener(cbk: EventListener<SolanaSwapData>): boolean {
|
|
488
|
-
const index = this.listeners.indexOf(cbk);
|
|
489
|
-
if(index>=0) {
|
|
490
|
-
this.listeners.splice(index, 1);
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
1
|
+
import {ChainEvents, ClaimEvent, EventListener, InitializeEvent, RefundEvent, SwapEvent} from "@atomiqlabs/base";
|
|
2
|
+
import {InitInstruction, SolanaSwapData} from "../swaps/SolanaSwapData";
|
|
3
|
+
import {IdlEvents} from "@coral-xyz/anchor";
|
|
4
|
+
import {SolanaSwapProgram} from "../swaps/SolanaSwapProgram";
|
|
5
|
+
import {
|
|
6
|
+
getLogger,
|
|
7
|
+
onceAsync, toEscrowHash, tryWithRetries
|
|
8
|
+
} from "../../utils/Utils";
|
|
9
|
+
import {ConfirmedSignatureInfo, Connection, ParsedTransactionWithMeta} from "@solana/web3.js";
|
|
10
|
+
import {SwapTypeEnum} from "../swaps/SwapTypeEnum";
|
|
11
|
+
import {
|
|
12
|
+
InstructionWithAccounts,
|
|
13
|
+
ProgramEvent
|
|
14
|
+
} from "../program/modules/SolanaProgramEvents";
|
|
15
|
+
import {SwapProgram} from "../swaps/v1/programTypes";
|
|
16
|
+
import {Buffer} from "buffer";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parsed event payload grouped by originating transaction metadata.
|
|
20
|
+
*
|
|
21
|
+
* @category Events
|
|
22
|
+
*/
|
|
23
|
+
export type EventObject = {
|
|
24
|
+
events: ProgramEvent<SwapProgram>[],
|
|
25
|
+
instructions?: (InstructionWithAccounts<SwapProgram> | null)[],
|
|
26
|
+
blockTime: number,
|
|
27
|
+
signature: string
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const LOG_FETCH_LIMIT = 500;
|
|
31
|
+
const PROCESSED_SIGNATURES_BACKLOG = 500;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Legacy current cursor of Solana event listener state.
|
|
35
|
+
*
|
|
36
|
+
* @category Events
|
|
37
|
+
*/
|
|
38
|
+
export type SolanaLegacyEventListenerState = {
|
|
39
|
+
/**
|
|
40
|
+
* Last processed transaction's signature
|
|
41
|
+
*/
|
|
42
|
+
signature: string,
|
|
43
|
+
/**
|
|
44
|
+
* Last processed transaction's slot
|
|
45
|
+
*/
|
|
46
|
+
slot: number
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Current cursor of Solana event listener state.
|
|
51
|
+
*
|
|
52
|
+
* @category Events
|
|
53
|
+
*/
|
|
54
|
+
export type SolanaEventListenerState = {
|
|
55
|
+
[version: string]: SolanaLegacyEventListenerState | null
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function toNewEventListenerState(obj: SolanaLegacyEventListenerState | SolanaEventListenerState | undefined): SolanaEventListenerState | undefined {
|
|
59
|
+
if(obj==null) return undefined;
|
|
60
|
+
if(obj.slot!=null || obj.signature!=null) return {"v1": obj} as SolanaEventListenerState;
|
|
61
|
+
return obj as SolanaEventListenerState;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Solana on-chain event handler for front-end systems without access to fs, uses pure WS to subscribe, might lose
|
|
66
|
+
* out on some events if the network is unreliable, front-end systems should take this into consideration and not
|
|
67
|
+
* rely purely on events
|
|
68
|
+
*
|
|
69
|
+
* @category Events
|
|
70
|
+
*/
|
|
71
|
+
export class SolanaChainEventsBrowser implements ChainEvents<SolanaSwapData, SolanaLegacyEventListenerState | SolanaEventListenerState> {
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
protected readonly listeners: EventListener<SolanaSwapData>[] = [];
|
|
77
|
+
/**
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
protected readonly connection: Connection;
|
|
81
|
+
/**
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
protected readonly contractVersions: {[version: string]: {swapContract: SolanaSwapProgram}};
|
|
85
|
+
/**
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
protected eventListeners: {[version: string]: number[]} = {};
|
|
89
|
+
/**
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
protected readonly logger = getLogger("SolanaChainEventsBrowser: ");
|
|
93
|
+
|
|
94
|
+
private readonly logFetchLimit: number;
|
|
95
|
+
private signaturesProcessing: {
|
|
96
|
+
[signature: string]: Promise<boolean>
|
|
97
|
+
} = {};
|
|
98
|
+
private processedSignatures: string[] = [];
|
|
99
|
+
private processedSignaturesIndex: number = 0;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
connection: Connection,
|
|
103
|
+
contractVersions: SolanaSwapProgram | {[version: string]: {swapContract: SolanaSwapProgram}},
|
|
104
|
+
logFetchLimit?: number
|
|
105
|
+
) {
|
|
106
|
+
this.connection = connection;
|
|
107
|
+
if(contractVersions instanceof SolanaSwapProgram) {
|
|
108
|
+
this.contractVersions = {[contractVersions.version]: {swapContract: contractVersions}};
|
|
109
|
+
} else {
|
|
110
|
+
this.contractVersions = contractVersions;
|
|
111
|
+
}
|
|
112
|
+
this.logFetchLimit = logFetchLimit ?? LOG_FETCH_LIMIT;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private addProcessedSignature(signature: string, version: string) {
|
|
116
|
+
this.processedSignatures[this.processedSignaturesIndex] = signature+"-"+version;
|
|
117
|
+
this.processedSignaturesIndex += 1;
|
|
118
|
+
if(this.processedSignaturesIndex >= PROCESSED_SIGNATURES_BACKLOG) this.processedSignaturesIndex = 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private isSignatureProcessed(signature: string, version: string): boolean {
|
|
122
|
+
return this.processedSignatures.includes(signature+"-"+version);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parses EventObject from the transaction
|
|
127
|
+
*
|
|
128
|
+
* @param transaction
|
|
129
|
+
* @param version
|
|
130
|
+
* @private
|
|
131
|
+
* @returns {EventObject} parsed event object
|
|
132
|
+
*/
|
|
133
|
+
private getEventObjectFromTransaction(transaction: ParsedTransactionWithMeta, version: string): EventObject | null {
|
|
134
|
+
const signature = transaction.transaction.signatures[0];
|
|
135
|
+
if(transaction.meta==null) throw new Error(`Transaction 'meta' not found for Solana tx: ${signature}`);
|
|
136
|
+
if(transaction.meta.err!=null || transaction.meta.logMessages==null) return null;
|
|
137
|
+
|
|
138
|
+
const instructions = this.contractVersions[version].swapContract._Events.decodeInstructions(transaction.transaction.message);
|
|
139
|
+
const events = this.contractVersions[version].swapContract._Events.parseLogs(transaction.meta.logMessages);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
instructions,
|
|
143
|
+
events,
|
|
144
|
+
blockTime: transaction.blockTime!,
|
|
145
|
+
signature: signature
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetches transaction from the RPC, parses it to even object & processes it through event handler
|
|
151
|
+
*
|
|
152
|
+
* @param signature
|
|
153
|
+
* @param version
|
|
154
|
+
* @private
|
|
155
|
+
* @returns {boolean} whether the operation was successful
|
|
156
|
+
*/
|
|
157
|
+
private async fetchTxAndProcessEvent(signature: string, version: string): Promise<boolean> {
|
|
158
|
+
try {
|
|
159
|
+
const transaction = await this.connection.getParsedTransaction(signature, {
|
|
160
|
+
commitment: "confirmed",
|
|
161
|
+
maxSupportedTransactionVersion: 1
|
|
162
|
+
});
|
|
163
|
+
if(transaction==null) return false;
|
|
164
|
+
|
|
165
|
+
const eventObject = this.getEventObjectFromTransaction(transaction, version);
|
|
166
|
+
if(eventObject==null) return true;
|
|
167
|
+
|
|
168
|
+
await this.processEvent(eventObject, version);
|
|
169
|
+
return true;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
this.logger.error("fetchTxAndProcessEvent(): Error fetching transaction and processing event, signature: "+signature, e);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetches and parses transaction instructions
|
|
178
|
+
*
|
|
179
|
+
* @private
|
|
180
|
+
* @returns {Promise<(InstructionWithAccounts<SwapProgram> | null)[] | null>} array of parsed instructions
|
|
181
|
+
*/
|
|
182
|
+
private async getTransactionInstructions(signature: string, version: string): Promise<(InstructionWithAccounts<SwapProgram> | null)[] | null> {
|
|
183
|
+
const transaction = await tryWithRetries<ParsedTransactionWithMeta>(async () => {
|
|
184
|
+
const res = await this.connection.getParsedTransaction(signature, {
|
|
185
|
+
commitment: "confirmed",
|
|
186
|
+
maxSupportedTransactionVersion: 0
|
|
187
|
+
});
|
|
188
|
+
if(res==null) throw new Error("Transaction not found!");
|
|
189
|
+
return res;
|
|
190
|
+
});
|
|
191
|
+
if(transaction.meta==null) throw new Error("Transaction 'meta' not found!");
|
|
192
|
+
if(transaction.meta.err!=null) return null;
|
|
193
|
+
return this.contractVersions[version].swapContract._Events.decodeInstructions(transaction.transaction.message);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns async getter for fetching on-demand initialize event swap data
|
|
198
|
+
*
|
|
199
|
+
* @param eventObject
|
|
200
|
+
* @param txoHash
|
|
201
|
+
* @param version
|
|
202
|
+
* @private
|
|
203
|
+
* @returns {() => Promise<SolanaSwapData>} getter to be passed to InitializeEvent constructor
|
|
204
|
+
*/
|
|
205
|
+
private getSwapDataGetter(eventObject: EventObject, txoHash: string, version: string): () => Promise<SolanaSwapData | null> {
|
|
206
|
+
return async () => {
|
|
207
|
+
if(eventObject.instructions==null) {
|
|
208
|
+
const ixs = await this.getTransactionInstructions(eventObject.signature, version);
|
|
209
|
+
if(ixs==null) return null;
|
|
210
|
+
eventObject.instructions = ixs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const initIx = eventObject.instructions.find(
|
|
214
|
+
ix => ix!=null && (ix.name === "offererInitializePayIn" || ix.name === "offererInitialize")
|
|
215
|
+
) as InitInstruction;
|
|
216
|
+
if(initIx == null) return null;
|
|
217
|
+
|
|
218
|
+
return SolanaSwapData.fromInstruction(this.contractVersions[version].swapContract.program.programId, version as "v1" | "v2", initIx, txoHash);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @internal
|
|
224
|
+
*/
|
|
225
|
+
protected parseInitializeEvent(data: IdlEvents<SwapProgram>["InitializeEvent"], eventObject: EventObject, version: string): InitializeEvent<SolanaSwapData> {
|
|
226
|
+
const paymentHash: string = Buffer.from(data.hash).toString("hex");
|
|
227
|
+
const txoHash: string = Buffer.from(data.txoHash).toString("hex");
|
|
228
|
+
const escrowHash = toEscrowHash(paymentHash, data.sequence);
|
|
229
|
+
this.logger.debug("InitializeEvent paymentHash: "+paymentHash+" sequence: "+data.sequence.toString(10)+
|
|
230
|
+
" txoHash: "+txoHash+" escrowHash: "+escrowHash);
|
|
231
|
+
return new InitializeEvent<SolanaSwapData>(
|
|
232
|
+
escrowHash,
|
|
233
|
+
SwapTypeEnum.toChainSwapType(data.kind),
|
|
234
|
+
onceAsync<SolanaSwapData | null>(this.getSwapDataGetter(eventObject, txoHash, version)),
|
|
235
|
+
version
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
protected parseRefundEvent(data: IdlEvents<SwapProgram>["RefundEvent"], version: string): RefundEvent<SolanaSwapData> {
|
|
243
|
+
const paymentHash: string = Buffer.from(data.hash).toString("hex");
|
|
244
|
+
const escrowHash = toEscrowHash(paymentHash, data.sequence);
|
|
245
|
+
this.logger.debug("RefundEvent paymentHash: "+paymentHash+" sequence: "+data.sequence.toString(10)+
|
|
246
|
+
" escrowHash: "+escrowHash);
|
|
247
|
+
return new RefundEvent<SolanaSwapData>(escrowHash, version);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @internal
|
|
252
|
+
*/
|
|
253
|
+
protected parseClaimEvent(data: IdlEvents<SwapProgram>["ClaimEvent"], version: string): ClaimEvent<SolanaSwapData> {
|
|
254
|
+
const secret: string = Buffer.from(data.secret).toString("hex");
|
|
255
|
+
const paymentHash: string = Buffer.from(data.hash).toString("hex");
|
|
256
|
+
const escrowHash = toEscrowHash(paymentHash, data.sequence);
|
|
257
|
+
this.logger.debug("ClaimEvent paymentHash: "+paymentHash+" sequence: "+data.sequence.toString(10)+
|
|
258
|
+
" secret: "+secret+" escrowHash: "+escrowHash);
|
|
259
|
+
return new ClaimEvent<SolanaSwapData>(escrowHash, secret, version);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Processes event as received from the chain, parses it & calls event listeners
|
|
264
|
+
*
|
|
265
|
+
* @param eventObject
|
|
266
|
+
* @param version
|
|
267
|
+
* @internal
|
|
268
|
+
*/
|
|
269
|
+
protected async processEvent(eventObject : EventObject, version: string) {
|
|
270
|
+
let parsedEvents: SwapEvent<SolanaSwapData>[] = eventObject.events.map(event => {
|
|
271
|
+
let parsedEvent: SwapEvent<SolanaSwapData> | undefined;
|
|
272
|
+
switch(event.name) {
|
|
273
|
+
case "ClaimEvent":
|
|
274
|
+
parsedEvent = this.parseClaimEvent(event.data, version);
|
|
275
|
+
break;
|
|
276
|
+
case "RefundEvent":
|
|
277
|
+
parsedEvent = this.parseRefundEvent(event.data, version);
|
|
278
|
+
break;
|
|
279
|
+
case "InitializeEvent":
|
|
280
|
+
parsedEvent = this.parseInitializeEvent(event.data, eventObject, version);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if(parsedEvent==null) return null;
|
|
284
|
+
(parsedEvent as any).meta = {
|
|
285
|
+
blockTime: eventObject.blockTime,
|
|
286
|
+
timestamp: eventObject.blockTime,
|
|
287
|
+
txId: eventObject.signature
|
|
288
|
+
};
|
|
289
|
+
return parsedEvent;
|
|
290
|
+
}).filter(parsedEvent => parsedEvent!=null);
|
|
291
|
+
|
|
292
|
+
for(let listener of this.listeners) {
|
|
293
|
+
await listener(parsedEvents);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Returns websocket event handler for specific event type
|
|
299
|
+
*
|
|
300
|
+
* @param name
|
|
301
|
+
* @param version
|
|
302
|
+
* @internal
|
|
303
|
+
* @returns event handler to be passed to program's addEventListener function
|
|
304
|
+
*/
|
|
305
|
+
protected getWsEventHandler<E extends "InitializeEvent" | "RefundEvent" | "ClaimEvent">(
|
|
306
|
+
name: E,
|
|
307
|
+
version: string
|
|
308
|
+
): (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => void {
|
|
309
|
+
return (data: IdlEvents<SwapProgram>[E], slotNumber: number, signature: string) => {
|
|
310
|
+
if(this.signaturesProcessing[signature+"-"+version]!=null) return;
|
|
311
|
+
if(this.isSignatureProcessed(signature, version)) return;
|
|
312
|
+
|
|
313
|
+
this.logger.debug("getWsEventHandler("+name+"): Process signature: ", signature);
|
|
314
|
+
|
|
315
|
+
this.signaturesProcessing[signature+"-"+version] = this.processEvent({
|
|
316
|
+
events: [{name, data: data as any}],
|
|
317
|
+
blockTime: Math.floor(Date.now()/1000),
|
|
318
|
+
signature
|
|
319
|
+
}, version).then(() => true).catch(e => {
|
|
320
|
+
this.logger.error("getWsEventHandler("+name+"): Error processing signature: "+signature, e);
|
|
321
|
+
return false;
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Sets up event handlers listening for swap events over websocket
|
|
328
|
+
*
|
|
329
|
+
* @internal
|
|
330
|
+
*/
|
|
331
|
+
protected setupWebsocket() {
|
|
332
|
+
for(let version in this.contractVersions) {
|
|
333
|
+
const program = this.contractVersions[version].swapContract.program;
|
|
334
|
+
const eventListeners = this.eventListeners[version] ??= [];
|
|
335
|
+
eventListeners.push(program.addEventListener<"InitializeEvent">("InitializeEvent", this.getWsEventHandler("InitializeEvent", version)));
|
|
336
|
+
eventListeners.push(program.addEventListener<"ClaimEvent">("ClaimEvent", this.getWsEventHandler("ClaimEvent", version)));
|
|
337
|
+
eventListeners.push(program.addEventListener<"RefundEvent">("RefundEvent", this.getWsEventHandler("RefundEvent", version)));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Gets all the new signatures from the last processed signature
|
|
343
|
+
*
|
|
344
|
+
* @param lastProcessedSignature
|
|
345
|
+
* @param version
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
private async getNewSignatures(lastProcessedSignature: {signature: string, slot: number}, version: string): Promise<ConfirmedSignatureInfo[] | null> {
|
|
349
|
+
let signatures: ConfirmedSignatureInfo[] = [];
|
|
350
|
+
|
|
351
|
+
let fetched = null;
|
|
352
|
+
while(fetched==null || fetched.length===this.logFetchLimit) {
|
|
353
|
+
if(signatures.length===0) {
|
|
354
|
+
fetched = await this.connection.getSignaturesForAddress(this.contractVersions[version].swapContract.program.programId, {
|
|
355
|
+
until: lastProcessedSignature.signature,
|
|
356
|
+
limit: this.logFetchLimit
|
|
357
|
+
}, "confirmed");
|
|
358
|
+
//Check if newest returned signature (index 0) is older than the latest signature's slot, this is a sanity check
|
|
359
|
+
if(fetched.length>0 && fetched[0].slot<lastProcessedSignature.slot) {
|
|
360
|
+
this.logger.debug("getNewSignatures(): Sanity check triggered, returned signature slot height is older than latest!");
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
fetched = await this.connection.getSignaturesForAddress(this.contractVersions[version].swapContract.program.programId, {
|
|
365
|
+
before: signatures[signatures.length-1].signature,
|
|
366
|
+
until: lastProcessedSignature.signature,
|
|
367
|
+
limit: this.logFetchLimit
|
|
368
|
+
}, "confirmed");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
signatures = signatures.concat(fetched);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return signatures;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Gets single latest known signature
|
|
379
|
+
*
|
|
380
|
+
* @private
|
|
381
|
+
*/
|
|
382
|
+
private async getFirstSignature(version: string): Promise<ConfirmedSignatureInfo[]> {
|
|
383
|
+
return await this.connection.getSignaturesForAddress(this.contractVersions[version].swapContract.program.programId, {
|
|
384
|
+
limit: 1
|
|
385
|
+
}, "confirmed");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Processes signatures, fetches transactions & processes event through event handlers
|
|
390
|
+
*
|
|
391
|
+
* @param signatures
|
|
392
|
+
* @param version
|
|
393
|
+
* @private
|
|
394
|
+
* @returns {Promise<{signature: string, slot: number}>} latest processed transaction signature and slot height
|
|
395
|
+
*/
|
|
396
|
+
private async processSignatures(signatures: ConfirmedSignatureInfo[], version: string): Promise<{signature: string, slot: number} | null> {
|
|
397
|
+
let lastSuccessfulSignature: {signature: string, slot: number} | null = null;
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
for(let i=signatures.length-1;i>=0;i--) {
|
|
401
|
+
const txSignature = signatures[i];
|
|
402
|
+
|
|
403
|
+
//Check if signature is already being processed by the
|
|
404
|
+
const signaturePromise = this.signaturesProcessing[txSignature.signature+"-"+version];
|
|
405
|
+
if(signaturePromise!=null) {
|
|
406
|
+
const result = await signaturePromise;
|
|
407
|
+
delete this.signaturesProcessing[txSignature.signature+"-"+version];
|
|
408
|
+
if(result) {
|
|
409
|
+
lastSuccessfulSignature = txSignature;
|
|
410
|
+
this.addProcessedSignature(txSignature.signature, version);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.logger.debug("processSignatures(): Process signature: ", txSignature);
|
|
416
|
+
|
|
417
|
+
const processPromise: Promise<boolean> = this.fetchTxAndProcessEvent(txSignature.signature, version);
|
|
418
|
+
this.signaturesProcessing[txSignature.signature+"-"+version] = processPromise;
|
|
419
|
+
|
|
420
|
+
const result = await processPromise;
|
|
421
|
+
if(!result) throw new Error("Failed to process signature: "+txSignature);
|
|
422
|
+
lastSuccessfulSignature = txSignature;
|
|
423
|
+
this.addProcessedSignature(txSignature.signature, version);
|
|
424
|
+
delete this.signaturesProcessing[txSignature.signature+"-"+version];
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
this.logger.error("processSignatures(): Failed processing signatures: ", e);
|
|
428
|
+
}
|
|
429
|
+
return lastSuccessfulSignature;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @inheritDoc
|
|
434
|
+
*/
|
|
435
|
+
async poll(lastSignature?: SolanaLegacyEventListenerState | SolanaEventListenerState): Promise<SolanaEventListenerState> {
|
|
436
|
+
const _lastSignature = toNewEventListenerState(lastSignature);
|
|
437
|
+
|
|
438
|
+
const result: SolanaEventListenerState = {};
|
|
439
|
+
for(let version in this.contractVersions) {
|
|
440
|
+
const lastSignature = _lastSignature?.[version];
|
|
441
|
+
|
|
442
|
+
let signatures = lastSignature==null
|
|
443
|
+
? await this.getFirstSignature(version)
|
|
444
|
+
: await this.getNewSignatures(lastSignature, version);
|
|
445
|
+
if(signatures==null) {
|
|
446
|
+
result[version] = lastSignature ?? null;
|
|
447
|
+
} else {
|
|
448
|
+
let lastSuccessfulSignature = await this.processSignatures(signatures, version);
|
|
449
|
+
result[version] = lastSuccessfulSignature ?? lastSignature ?? null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* @inheritDoc
|
|
458
|
+
*/
|
|
459
|
+
init(noAutomaticPoll?: boolean): Promise<void> {
|
|
460
|
+
if(noAutomaticPoll) return Promise.resolve();
|
|
461
|
+
this.setupWebsocket();
|
|
462
|
+
return Promise.resolve();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @inheritDoc
|
|
467
|
+
*/
|
|
468
|
+
async stop(): Promise<void> {
|
|
469
|
+
for(let version in this.eventListeners) {
|
|
470
|
+
for(let num of this.eventListeners[version]) {
|
|
471
|
+
await this.contractVersions[version].swapContract.program.removeEventListener(num);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
this.eventListeners = {};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @inheritDoc
|
|
479
|
+
*/
|
|
480
|
+
registerListener(cbk: EventListener<SolanaSwapData>): void {
|
|
481
|
+
this.listeners.push(cbk);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @inheritDoc
|
|
486
|
+
*/
|
|
487
|
+
unregisterListener(cbk: EventListener<SolanaSwapData>): boolean {
|
|
488
|
+
const index = this.listeners.indexOf(cbk);
|
|
489
|
+
if(index>=0) {
|
|
490
|
+
this.listeners.splice(index, 1);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|