@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.
Files changed (131) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +73 -73
  3. package/dist/index.d.ts +81 -81
  4. package/dist/index.js +102 -102
  5. package/dist/node/index.d.ts +9 -9
  6. package/dist/node/index.js +13 -13
  7. package/dist/solana/SolanaChainType.d.ts +15 -15
  8. package/dist/solana/SolanaChainType.js +2 -2
  9. package/dist/solana/SolanaChains.d.ts +12 -12
  10. package/dist/solana/SolanaChains.js +45 -45
  11. package/dist/solana/SolanaInitializer.d.ts +94 -94
  12. package/dist/solana/SolanaInitializer.js +174 -174
  13. package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +222 -222
  14. package/dist/solana/btcrelay/SolanaBtcRelay.js +455 -455
  15. package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +84 -84
  16. package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +70 -70
  17. package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +92 -92
  18. package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +109 -109
  19. package/dist/solana/btcrelay/program/programIdl.json +671 -671
  20. package/dist/solana/chain/SolanaAction.d.ts +26 -26
  21. package/dist/solana/chain/SolanaAction.js +87 -87
  22. package/dist/solana/chain/SolanaChainInterface.d.ts +224 -224
  23. package/dist/solana/chain/SolanaChainInterface.js +275 -275
  24. package/dist/solana/chain/SolanaModule.d.ts +14 -14
  25. package/dist/solana/chain/SolanaModule.js +13 -13
  26. package/dist/solana/chain/modules/SolanaAddresses.d.ts +8 -8
  27. package/dist/solana/chain/modules/SolanaAddresses.js +22 -22
  28. package/dist/solana/chain/modules/SolanaBlocks.d.ts +32 -32
  29. package/dist/solana/chain/modules/SolanaBlocks.js +78 -78
  30. package/dist/solana/chain/modules/SolanaEvents.d.ts +68 -68
  31. package/dist/solana/chain/modules/SolanaEvents.js +238 -238
  32. package/dist/solana/chain/modules/SolanaFees.d.ts +189 -189
  33. package/dist/solana/chain/modules/SolanaFees.js +434 -434
  34. package/dist/solana/chain/modules/SolanaSignatures.d.ts +24 -24
  35. package/dist/solana/chain/modules/SolanaSignatures.js +39 -39
  36. package/dist/solana/chain/modules/SolanaSlots.d.ts +33 -33
  37. package/dist/solana/chain/modules/SolanaSlots.js +72 -72
  38. package/dist/solana/chain/modules/SolanaTokens.d.ts +123 -123
  39. package/dist/solana/chain/modules/SolanaTokens.js +242 -242
  40. package/dist/solana/chain/modules/SolanaTransactions.d.ts +149 -149
  41. package/dist/solana/chain/modules/SolanaTransactions.js +445 -445
  42. package/dist/solana/connection/ConnectionWithRetries.d.ts +35 -35
  43. package/dist/solana/connection/ConnectionWithRetries.js +86 -71
  44. package/dist/solana/events/SolanaChainEvents.d.ts +45 -45
  45. package/dist/solana/events/SolanaChainEvents.js +108 -108
  46. package/dist/solana/events/SolanaChainEventsBrowser.d.ts +205 -205
  47. package/dist/solana/events/SolanaChainEventsBrowser.js +404 -404
  48. package/dist/solana/program/SolanaProgramBase.d.ts +73 -73
  49. package/dist/solana/program/SolanaProgramBase.js +54 -54
  50. package/dist/solana/program/SolanaProgramModule.d.ts +8 -8
  51. package/dist/solana/program/SolanaProgramModule.js +11 -11
  52. package/dist/solana/program/modules/SolanaProgramEvents.d.ts +53 -53
  53. package/dist/solana/program/modules/SolanaProgramEvents.js +117 -117
  54. package/dist/solana/swaps/SolanaSwapData.d.ts +333 -333
  55. package/dist/solana/swaps/SolanaSwapData.js +535 -535
  56. package/dist/solana/swaps/SolanaSwapModule.d.ts +11 -11
  57. package/dist/solana/swaps/SolanaSwapModule.js +12 -12
  58. package/dist/solana/swaps/SolanaSwapProgram.d.ts +376 -376
  59. package/dist/solana/swaps/SolanaSwapProgram.js +769 -769
  60. package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -11
  61. package/dist/solana/swaps/SwapTypeEnum.js +43 -43
  62. package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +95 -95
  63. package/dist/solana/swaps/modules/SolanaDataAccount.js +232 -232
  64. package/dist/solana/swaps/modules/SolanaLpVault.d.ts +69 -69
  65. package/dist/solana/swaps/modules/SolanaLpVault.js +171 -171
  66. package/dist/solana/swaps/modules/SwapClaim.d.ts +126 -126
  67. package/dist/solana/swaps/modules/SwapClaim.js +294 -294
  68. package/dist/solana/swaps/modules/SwapInit.d.ts +213 -213
  69. package/dist/solana/swaps/modules/SwapInit.js +658 -658
  70. package/dist/solana/swaps/modules/SwapRefund.d.ts +87 -87
  71. package/dist/solana/swaps/modules/SwapRefund.js +293 -293
  72. package/dist/solana/swaps/programIdl.json +945 -945
  73. package/dist/solana/swaps/programTypes.d.ts +943 -943
  74. package/dist/solana/swaps/programTypes.js +945 -945
  75. package/dist/solana/swaps/v1/programIdl.json +945 -945
  76. package/dist/solana/swaps/v1/programTypes.d.ts +943 -943
  77. package/dist/solana/swaps/v1/programTypes.js +945 -945
  78. package/dist/solana/swaps/v2/programIdl.json +952 -952
  79. package/dist/solana/swaps/v2/programTypes.d.ts +950 -950
  80. package/dist/solana/swaps/v2/programTypes.js +952 -952
  81. package/dist/solana/wallet/SolanaKeypairWallet.d.ts +29 -29
  82. package/dist/solana/wallet/SolanaKeypairWallet.js +50 -50
  83. package/dist/solana/wallet/SolanaSigner.d.ts +30 -30
  84. package/dist/solana/wallet/SolanaSigner.js +30 -30
  85. package/dist/utils/Utils.d.ts +58 -58
  86. package/dist/utils/Utils.js +170 -170
  87. package/node/index.d.ts +1 -1
  88. package/node/index.js +3 -3
  89. package/package.json +46 -46
  90. package/src/index.ts +87 -87
  91. package/src/node/index.ts +9 -9
  92. package/src/solana/SolanaChainType.ts +32 -32
  93. package/src/solana/SolanaChains.ts +46 -46
  94. package/src/solana/SolanaInitializer.ts +278 -278
  95. package/src/solana/btcrelay/SolanaBtcRelay.ts +615 -615
  96. package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +116 -116
  97. package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +148 -148
  98. package/src/solana/btcrelay/program/programIdl.json +670 -670
  99. package/src/solana/chain/SolanaAction.ts +109 -109
  100. package/src/solana/chain/SolanaChainInterface.ts +404 -404
  101. package/src/solana/chain/SolanaModule.ts +20 -20
  102. package/src/solana/chain/modules/SolanaAddresses.ts +20 -20
  103. package/src/solana/chain/modules/SolanaBlocks.ts +89 -89
  104. package/src/solana/chain/modules/SolanaEvents.ts +271 -271
  105. package/src/solana/chain/modules/SolanaFees.ts +522 -522
  106. package/src/solana/chain/modules/SolanaSignatures.ts +39 -39
  107. package/src/solana/chain/modules/SolanaSlots.ts +85 -85
  108. package/src/solana/chain/modules/SolanaTokens.ts +300 -300
  109. package/src/solana/chain/modules/SolanaTransactions.ts +503 -503
  110. package/src/solana/connection/ConnectionWithRetries.ts +113 -96
  111. package/src/solana/events/SolanaChainEvents.ts +127 -127
  112. package/src/solana/events/SolanaChainEventsBrowser.ts +495 -495
  113. package/src/solana/program/SolanaProgramBase.ts +119 -119
  114. package/src/solana/program/SolanaProgramModule.ts +15 -15
  115. package/src/solana/program/modules/SolanaProgramEvents.ts +157 -157
  116. package/src/solana/swaps/SolanaSwapData.ts +735 -735
  117. package/src/solana/swaps/SolanaSwapModule.ts +19 -19
  118. package/src/solana/swaps/SolanaSwapProgram.ts +1074 -1074
  119. package/src/solana/swaps/SwapTypeEnum.ts +30 -30
  120. package/src/solana/swaps/modules/SolanaDataAccount.ts +302 -302
  121. package/src/solana/swaps/modules/SolanaLpVault.ts +208 -208
  122. package/src/solana/swaps/modules/SwapClaim.ts +387 -387
  123. package/src/solana/swaps/modules/SwapInit.ts +785 -785
  124. package/src/solana/swaps/modules/SwapRefund.ts +353 -353
  125. package/src/solana/swaps/v1/programIdl.json +944 -944
  126. package/src/solana/swaps/v1/programTypes.ts +1885 -1885
  127. package/src/solana/swaps/v2/programIdl.json +951 -951
  128. package/src/solana/swaps/v2/programTypes.ts +1899 -1899
  129. package/src/solana/wallet/SolanaKeypairWallet.ts +56 -56
  130. package/src/solana/wallet/SolanaSigner.ts +43 -43
  131. package/src/utils/Utils.ts +194 -194
@@ -1,504 +1,504 @@
1
- import {
2
- ComputeBudgetInstruction,
3
- ComputeBudgetProgram, Finality, Keypair,
4
- SendOptions, SignatureResult, Signer, Transaction,
5
- TransactionExpiredBlockheightExceededError
6
- } from "@solana/web3.js";
7
- import {SolanaModule} from "../SolanaModule";
8
- // @ts-ignore
9
- import * as bs58 from "bs58";
10
- import {tryWithRetries} from "../../../utils/Utils";
11
- import {Buffer} from "buffer";
12
- import {SolanaSigner} from "../../wallet/SolanaSigner";
13
- import {TransactionRevertedError} from "@atomiqlabs/base";
14
-
15
- /**
16
- * Solana unsigned transaction type, optionally also contains additional signers which should be used to additionally
17
- * sign the transaction.
18
- *
19
- * @category Chain Interface
20
- */
21
- export type SolanaTx = {tx: Transaction, signers: Signer[]};
22
- /**
23
- * Alias for signed Solana transaction, uses the native transaction type of `@solana/web3.js`
24
- *
25
- * @category Chain Interface
26
- */
27
- export type SignedSolanaTx = Transaction;
28
-
29
- export class SolanaTransactions extends SolanaModule {
30
-
31
- private cbkBeforeTxSigned?: (tx: SolanaTx) => Promise<void>;
32
- /**
33
- * Callback for sending transaction, returns not null if it was successfully able to send the transaction, and null
34
- * if the transaction should be sent through other means)
35
- * @private
36
- */
37
- private cbkSendTransaction?: (tx: Buffer, options?: SendOptions) => Promise<string>;
38
-
39
- /**
40
- * Sends raw solana transaction, first through the cbkSendTransaction callback (for e.g. sending the transaction
41
- * to a different specific RPC), the through the Fees handler (for e.g. Jito transaction) and last through the
42
- * underlying provider's Connection instance (the usual way). Only sends the transaction through one channel.
43
- *
44
- * @param data
45
- * @param options
46
- * @private
47
- */
48
- private async sendRawTransaction(data: Buffer, options?: SendOptions): Promise<string> {
49
- let result: string | null = null;
50
- options ??= {};
51
- options.maxRetries = 0;
52
- if(this.cbkSendTransaction!=null) result = await this.cbkSendTransaction(data, options);
53
- if(result==null) result = await this.root.Fees.submitTx(data, options);
54
- if(result==null) result = await this.connection.sendRawTransaction(data, options);
55
- // this.logger.debug("sendRawTransaction(): tx sent, signature: "+result);
56
- return result;
57
- }
58
-
59
- /**
60
- * Waits for the transaction to confirm by periodically checking the transaction status over HTTP, also
61
- * re-sends the transaction at regular intervals
62
- *
63
- * @param tx solana tx to wait for confirmation for
64
- * @param finality wait for this finality
65
- * @param abortSignal signal to abort waiting for tx confirmation
66
- * @private
67
- */
68
- private txConfirmationAndResendWatchdog(
69
- tx: Transaction,
70
- finality?: Finality,
71
- abortSignal?: AbortSignal
72
- ): Promise<string> {
73
- if(tx.signature==null) throw new Error("Cannot check confirmation status of tx without signature!");
74
- const rawTx = tx.serialize();
75
- const signature = bs58.encode(tx.signature);
76
- return new Promise((resolve, reject) => {
77
- let watchdogInterval: any;
78
- watchdogInterval = setInterval(async () => {
79
- const result = await this.sendRawTransaction(rawTx, {skipPreflight: true}).catch(
80
- e => this.logger.error("txConfirmationAndResendWatchdog(): transaction re-sent error: ", e)
81
- );
82
- this.logger.debug("txConfirmationAndResendWatchdog(): transaction re-sent: "+result);
83
-
84
- let status = await this.getTxIdStatus(signature, finality).catch(
85
- e => this.logger.error("txConfirmationAndResendWatchdog(): get tx id status error: ", e)
86
- );
87
- if(status==null || status==="not_found") {
88
- const blockValidity = await this.connection.isBlockhashValid(tx.recentBlockhash!, {commitment: finality}).catch(
89
- e => this.logger.error("txConfirmationAndResendWatchdog(): blockhash validity check error: ", e)
90
- );
91
- if(!blockValidity) return;
92
- if(blockValidity.value) return;
93
- try {
94
- //One last try to get the txId status
95
- const statusCheck = await this.getTxIdStatus(signature, finality);
96
- if(statusCheck === "not_found") reject(new Error("Transaction expired before confirmation, please try again!"));
97
- status = statusCheck;
98
- } catch (e) {
99
- this.logger.error("txConfirmationAndResendWatchdog(): re-check get tx id status error: ", e);
100
- return;
101
- }
102
- }
103
- if(status==="success") {
104
- this.logger.info("txConfirmationAndResendWatchdog(): transaction confirmed from HTTP polling, signature: "+signature);
105
- resolve(signature);
106
- }
107
- if(status==="reverted") reject(new TransactionRevertedError("Transaction reverted!"));
108
- clearInterval(watchdogInterval);
109
- }, this.retryPolicy?.transactionResendInterval || 3000);
110
-
111
- if(abortSignal!=null) abortSignal.addEventListener("abort", () => {
112
- clearInterval(watchdogInterval);
113
- reject(abortSignal.reason);
114
- });
115
- })
116
- }
117
-
118
- /**
119
- * Waits for the transaction to confirm from WS, sometimes the WS rejects even though the transaction was confirmed
120
- * this therefore also runs an ultimate check on the transaction in case the WS handler rejects, checking if it
121
- * really was expired
122
- *
123
- * @param tx solana tx to wait for confirmation for
124
- * @param finality wait for this finality
125
- * @param abortSignal signal to abort waiting for tx confirmation
126
- * @private
127
- */
128
- private async txConfirmFromWebsocket(
129
- tx: Transaction,
130
- finality?: Finality,
131
- abortSignal?: AbortSignal
132
- ): Promise<string> {
133
- if(tx.signature==null) throw new Error("Cannot wait for confirmation for tx without signature!");
134
- const signature = bs58.encode(tx.signature);
135
-
136
- let result: SignatureResult;
137
- try {
138
- result = await new Promise<SignatureResult>((resolve, reject) => {
139
- let subscriptionId: number | undefined;
140
-
141
- if(abortSignal!=null) abortSignal.addEventListener("abort", () => {
142
- if(subscriptionId!=null) this.connection.removeSignatureListener(subscriptionId).catch(e => {
143
- this.logger.debug("txConfirmFromWebsocket(): remove WS signature confirm listener error: ", e);
144
- });
145
- subscriptionId = undefined;
146
- reject(abortSignal.reason);
147
- });
148
-
149
- subscriptionId = this.connection.onSignature(signature, (data) => {
150
- resolve(data);
151
- subscriptionId = undefined;
152
- }, finality);
153
- });
154
- this.logger.info("txConfirmFromWebsocket(): transaction confirmed from WS, signature: "+signature);
155
- } catch (err: any) {
156
- if(abortSignal!=null && abortSignal.aborted) throw err;
157
- this.logger.debug("txConfirmFromWebsocket(): transaction rejected from WS, running ultimate check, expiry blockheight: "+tx.lastValidBlockHeight+" signature: "+signature+" error: "+err);
158
- const status = await tryWithRetries(
159
- () => this.getTxIdStatus(signature, finality)
160
- );
161
- this.logger.info("txConfirmFromWebsocket(): transaction status: "+status+" signature: "+signature);
162
- if(status==="success") return signature;
163
- if(status==="reverted") throw new TransactionRevertedError("Transaction reverted!");
164
- if(err instanceof TransactionExpiredBlockheightExceededError || err.toString().startsWith("TransactionExpiredBlockheightExceededError")) {
165
- throw new Error("Transaction expired before confirmation, please try again!");
166
- } else {
167
- throw err;
168
- }
169
- }
170
- if(result.err!=null) throw new TransactionRevertedError("Transaction reverted!");
171
- return signature;
172
- }
173
-
174
- /**
175
- * Waits for transaction confirmation using WS subscription and occasional HTTP polling, also re-sends
176
- * the transaction at regular interval
177
- *
178
- * @param tx solana transaction to wait for confirmation for & keep re-sending until it confirms
179
- * @param abortSignal signal to abort waiting for tx confirmation
180
- * @param finality wait for specific finality
181
- * @private
182
- */
183
- private async confirmTransaction(tx: Transaction, abortSignal?: AbortSignal, finality?: Finality) {
184
- const abortController = new AbortController();
185
- if(abortSignal!=null) {
186
- abortSignal.throwIfAborted();
187
- abortSignal.addEventListener("abort", () => {
188
- abortController.abort(abortSignal.reason);
189
- });
190
- }
191
-
192
- let txSignature: string;
193
- try {
194
- txSignature = await Promise.race([
195
- this.txConfirmationAndResendWatchdog(tx, finality, abortController.signal),
196
- this.txConfirmFromWebsocket(tx, finality, abortController.signal)
197
- ]);
198
- } catch (e) {
199
- abortController.abort(e);
200
- throw e;
201
- }
202
-
203
- // this.logger.info("confirmTransaction(): transaction confirmed, signature: "+txSignature);
204
-
205
- abortController.abort();
206
- }
207
-
208
- /**
209
- * Prepares solana transactions, assigns recentBlockhash if needed, applies Phantom hotfix,
210
- * sets feePayer to ourselves, calls beforeTxSigned callback & signs transaction with provided signers array
211
- *
212
- * @param txs
213
- * @param signer
214
- */
215
- public async prepareTransactions(txs: SolanaTx[], signer?: SolanaSigner): Promise<void> {
216
- if(txs.length===0) return;
217
- const signerAddress = signer?.getPublicKey() ?? txs[0].tx.feePayer;
218
- if(signerAddress==null) throw new Error("Cannot get tx sender address!");
219
-
220
- let latestBlockData: {blockhash: string, lastValidBlockHeight: number} | null = null;
221
-
222
- for(let tx of txs) {
223
- if(tx.tx.recentBlockhash==null) {
224
- if(latestBlockData==null) {
225
- latestBlockData = await this.connection.getLatestBlockhash("confirmed");
226
- this.logger.debug("prepareTransactions(): fetched latest block data for transactions," +
227
- " blockhash: "+latestBlockData.blockhash+" expiry blockheight: "+latestBlockData.lastValidBlockHeight);
228
- }
229
- tx.tx.recentBlockhash = latestBlockData.blockhash;
230
- tx.tx.lastValidBlockHeight = latestBlockData.lastValidBlockHeight;
231
- }
232
-
233
- //This is a hotfix for Phantom adding compute unit price instruction on the first position & breaking
234
- // required instructions order (e.g. btc relay verify needs to be 0th instruction in a tx)
235
- if((signer==null || signer.keypair==null) && tx.tx.signatures.length===0) {
236
- const foundIx = tx.tx.instructions.find(ix => ix.programId.equals(ComputeBudgetProgram.programId) && ComputeBudgetInstruction.decodeInstructionType(ix)==="SetComputeUnitPrice")
237
- if(foundIx==null) tx.tx.instructions.splice(tx.tx.instructions.length-1, 0, ComputeBudgetProgram.setComputeUnitPrice({microLamports: 1}));
238
- }
239
-
240
- tx.tx.feePayer = signerAddress;
241
- if(signer!=null && this.cbkBeforeTxSigned!=null) await this.cbkBeforeTxSigned(tx);
242
- if(tx.signers!=null && tx.signers.length>0) for(let signer of tx.signers) tx.tx.sign(signer);
243
- }
244
- }
245
-
246
- /**
247
- * Sends out a signed transaction to the RPC
248
- *
249
- * @param tx solana tx to send
250
- * @param options send options to be passed to the RPC
251
- * @param onBeforePublish a callback called before every transaction is published
252
- * @private
253
- */
254
- private async sendSignedTransaction(tx: Transaction, options?: SendOptions, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string> {
255
- if(tx.signature==null) throw new Error("Cannot broadcast tx without signature!");
256
- const signature = bs58.encode(tx.signature);
257
-
258
- if(onBeforePublish!=null) await onBeforePublish(signature, this.serializeSignedTx(tx));
259
- const serializedTx = tx.serialize();
260
- this.logger.debug("sendSignedTransaction(): sending transaction: "+serializedTx.toString("hex")+
261
- " signature: "+signature);
262
- const txResult = await tryWithRetries(() => this.sendRawTransaction(serializedTx, options), this.retryPolicy);
263
- this.logger.info("sendSignedTransaction(): tx sent, signature: "+txResult);
264
- return txResult;
265
- }
266
-
267
- /**
268
- * Prepares (adds recent blockhash if required, applies Phantom hotfix),
269
- * signs (all together using signAllTransactions() calls), sends (in parallel or sequentially) &
270
- * optionally waits for confirmation of a batch of solana transactions
271
- *
272
- * @param signer
273
- * @param _txs
274
- * @param waitForConfirmation whether to wait for transaction confirmations (this also makes sure the transactions
275
- * are re-sent at regular intervals)
276
- * @param abortSignal abort signal to abort waiting for transaction confirmations
277
- * @param parallel whether the send all the transaction at once in parallel or sequentially (such that transactions
278
- * are executed in order)
279
- * @param onBeforePublish a callback called before every transaction is published
280
- */
281
- public async sendAndConfirm(signer: SolanaSigner, _txs: SolanaTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal, parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string[]> {
282
- const options = {
283
- skipPreflight: true
284
- };
285
-
286
- this.logger.debug("sendAndConfirm(): sending transactions, count: "+_txs.length+
287
- " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
288
-
289
- const BATCH_SIZE = 50;
290
-
291
- const signatures: string[] = [];
292
- for(let e=0;e<_txs.length;e+=BATCH_SIZE) {
293
- const txs = _txs.slice(e, e+BATCH_SIZE);
294
-
295
- await this.prepareTransactions(txs, signer)
296
- const signedTxs = await signer.wallet.signAllTransactions(txs.map(e => e.tx));
297
- signedTxs.forEach((tx, index) => {
298
- const solTx = txs[index];
299
- tx.lastValidBlockHeight = solTx.tx.lastValidBlockHeight;
300
- solTx.tx = tx
301
- });
302
- this.logger.debug("sendAndConfirm(): sending transaction batch ("+e+".."+(e+50)+"), count: "+txs.length);
303
-
304
- //For solana we are forced to send txs one-by-one even with parallel, as we cannot determine their order upfront,
305
- // however e.g. Jito could possibly handle sending a single package of up to 5 txns in order.
306
- for(let i=0;i<txs.length;i++) {
307
- const solTx = txs[i];
308
- const signature = await this.sendSignedTransaction(solTx.tx, options, onBeforePublish);
309
- const confirmPromise = this.confirmTransaction(solTx.tx, abortSignal, "confirmed");
310
- //Don't await the last promise when !waitForConfirmation
311
- if(i<txs.length-1 || e+50<_txs.length || waitForConfirmation) {
312
- await confirmPromise;
313
- } else {
314
- confirmPromise.catch(err => this.logger.error(`sendAndConfirm(): Error while awaiting confirmation of ${signature}: `, err));
315
- }
316
- signatures.push(signature);
317
- }
318
- }
319
-
320
- this.logger.info("sendAndConfirm(): sent transactions, count: "+_txs.length+
321
- " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
322
-
323
- return signatures;
324
- }
325
-
326
- public async sendSignedAndConfirm(
327
- signedTxs: SignedSolanaTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal,
328
- parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>
329
- ): Promise<string[]> {
330
- //Verify all txns are properly signed
331
- signedTxs.forEach(val => {
332
- const pubkeysSigned: Set<string> = new Set<string>(
333
- val.signatures.map(val => val.publicKey.toString())
334
- );
335
- val.instructions.forEach(ix => {
336
- ix.keys.forEach(key => {
337
- if(key.isSigner && !pubkeysSigned.has(key.pubkey.toString()))
338
- throw new Error("Transaction requires signature by: "+key.pubkey.toString());
339
- });
340
- });
341
- });
342
-
343
- const options = {
344
- skipPreflight: true
345
- };
346
-
347
- this.logger.debug("sendSignedAndConfirm(): sending transactions, count: "+signedTxs.length+
348
- " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
349
-
350
- const abortController = new AbortController();
351
- if(abortSignal!=null) {
352
- abortSignal.throwIfAborted();
353
- abortSignal.addEventListener("abort", () => abortController.abort(abortSignal.reason));
354
- }
355
-
356
- const signatures: string[] = [];
357
- const promises: Promise<void>[] = [];
358
- for(let i=0; i<signedTxs.length; i++) {
359
- const signedTx = signedTxs[i];
360
- this.logger.debug("sendSignedAndConfirm(): sending transaction "+i+", total count: "+signedTxs.length);
361
- const signature = await this.sendSignedTransaction(signedTx, options, onBeforePublish);
362
- if(abortSignal!=null) abortSignal.throwIfAborted();
363
- const confirmPromise = this.confirmTransaction(signedTx, abortController.signal, "confirmed");
364
- signatures.push(signature);
365
- if(!parallel) {
366
- //Don't await the last one when not wait for confirmations
367
- if(i<signedTxs.length-1 || waitForConfirmation) {
368
- await confirmPromise;
369
- continue;
370
- }
371
- } else {
372
- if(waitForConfirmation) {
373
- promises.push(confirmPromise.catch(err => {
374
- this.logger.error(`sendSignedAndConfirm(): Error while awaiting confirmation of ${signature}: `, err);
375
- abortController.abort(err);
376
- }));
377
- continue;
378
- }
379
- }
380
- confirmPromise.catch(err => this.logger.error(`sendSignedAndConfirm(): Error while awaiting confirmation of ${signature}: `, err));
381
- }
382
-
383
- abortController.signal.throwIfAborted();
384
- if(parallel && waitForConfirmation) await Promise.all(promises);
385
- abortController.signal.throwIfAborted();
386
-
387
- this.logger.info("sendSignedAndConfirm(): sent transactions, count: "+signedTxs.length+
388
- " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
389
-
390
- return signatures;
391
- }
392
-
393
- /**
394
- * Serializes the solana transaction, saves the transaction, signers & last valid blockheight
395
- *
396
- * @param tx
397
- */
398
- public serializeUnsignedTx(tx: SolanaTx): string {
399
- return JSON.stringify({
400
- tx: tx.tx.serialize({requireAllSignatures: false, verifySignatures: false}).toString("hex"),
401
- signers: tx.signers.map(e => Buffer.from(e.secretKey).toString("hex")),
402
- lastValidBlockheight: tx.tx.lastValidBlockHeight
403
- });
404
- }
405
-
406
- /**
407
- * Serializes the solana transaction
408
- *
409
- * @param signedTx
410
- */
411
- public serializeSignedTx(signedTx: Transaction): string {
412
- return signedTx.serialize({requireAllSignatures: false, verifySignatures: false}).toString("hex");
413
- }
414
-
415
- /**
416
- * Deserializes saved solana transaction, extracting the transaction, signers & last valid blockheight
417
- *
418
- * @param txData
419
- */
420
- public deserializeUnsignedTx(txData: string): SolanaTx {
421
- const jsonParsed: {
422
- tx: string,
423
- signers: string[],
424
- lastValidBlockheight: number
425
- } = JSON.parse(txData);
426
-
427
- const transaction = Transaction.from(Buffer.from(jsonParsed.tx, "hex"));
428
- transaction.lastValidBlockHeight = jsonParsed.lastValidBlockheight;
429
-
430
- return {
431
- tx: transaction,
432
- signers: jsonParsed.signers.map(e => Keypair.fromSecretKey(Buffer.from(e, "hex"))),
433
- };
434
- }
435
-
436
- /**
437
- * Deserializes raw solana transaction
438
- *
439
- * @param txData
440
- */
441
- public deserializeSignedTransaction(txData: string): Transaction {
442
- return Transaction.from(Buffer.from(txData, "hex"));
443
- }
444
-
445
- /**
446
- * Gets the status of the raw solana transaction, this also checks transaction expiry & can therefore report tx
447
- * in "pending" status, however pending status doesn't necessarily mean that the transaction was sent (again,
448
- * no mempool on Solana, cannot check that), this function is preferred against getTxIdStatus
449
- *
450
- * @param tx
451
- */
452
- public async getTxStatus(tx: string): Promise<"pending" | "success" | "not_found" | "reverted"> {
453
- const parsedTx: Transaction = this.deserializeSignedTransaction(tx);
454
- const signature = bs58.encode(parsedTx.signature!);
455
- const txReceipt = await this.connection.getTransaction(signature, {
456
- commitment: "confirmed",
457
- maxSupportedTransactionVersion: 0
458
- });
459
- if(txReceipt==null) {
460
- const {value: isValid} = await this.connection.isBlockhashValid(parsedTx.recentBlockhash!, {commitment: "processed"});
461
- if(!isValid) return "not_found";
462
- return "pending";
463
- }
464
- if(txReceipt.meta==null) throw new Error(`Cannot read status (meta) of Solana transaction: ${signature}`);
465
- if(txReceipt.meta.err) return "reverted";
466
- return "success";
467
- }
468
-
469
- /**
470
- * Gets the status of the solana transaction with a specific txId, this cannot report whether the transaction is
471
- * "pending" because Solana has no concept of mempool & only confirmed transactions are accessible
472
- *
473
- * @param txId
474
- * @param finality
475
- */
476
- public async getTxIdStatus(txId: string, finality?: Finality): Promise<"success" | "not_found" | "reverted"> {
477
- const txReceipt = await this.connection.getTransaction(txId, {
478
- commitment: finality || "confirmed",
479
- maxSupportedTransactionVersion: 0
480
- });
481
- if(txReceipt==null) return "not_found";
482
- if(txReceipt.meta==null) throw new Error(`Cannot read status (meta) of Solana transaction: ${txId}`);
483
- if(txReceipt.meta.err) return "reverted";
484
- return "success";
485
- }
486
-
487
- public onBeforeTxSigned(callback: (tx: SolanaTx) => Promise<void>): void {
488
- this.cbkBeforeTxSigned = callback;
489
- }
490
-
491
- public offBeforeTxSigned(callback: (tx: SolanaTx) => Promise<void>): boolean {
492
- delete this.cbkBeforeTxSigned;
493
- return true;
494
- }
495
-
496
- public onSendTransaction(callback: (tx: Buffer, options?: SendOptions) => Promise<string>): void {
497
- this.cbkSendTransaction = callback;
498
- }
499
-
500
- public offSendTransaction(callback: (tx: Buffer, options?: SendOptions) => Promise<string>): boolean {
501
- delete this.cbkSendTransaction;
502
- return true;
503
- }
1
+ import {
2
+ ComputeBudgetInstruction,
3
+ ComputeBudgetProgram, Finality, Keypair,
4
+ SendOptions, SignatureResult, Signer, Transaction,
5
+ TransactionExpiredBlockheightExceededError
6
+ } from "@solana/web3.js";
7
+ import {SolanaModule} from "../SolanaModule";
8
+ // @ts-ignore
9
+ import * as bs58 from "bs58";
10
+ import {tryWithRetries} from "../../../utils/Utils";
11
+ import {Buffer} from "buffer";
12
+ import {SolanaSigner} from "../../wallet/SolanaSigner";
13
+ import {TransactionRevertedError} from "@atomiqlabs/base";
14
+
15
+ /**
16
+ * Solana unsigned transaction type, optionally also contains additional signers which should be used to additionally
17
+ * sign the transaction.
18
+ *
19
+ * @category Chain Interface
20
+ */
21
+ export type SolanaTx = {tx: Transaction, signers: Signer[]};
22
+ /**
23
+ * Alias for signed Solana transaction, uses the native transaction type of `@solana/web3.js`
24
+ *
25
+ * @category Chain Interface
26
+ */
27
+ export type SignedSolanaTx = Transaction;
28
+
29
+ export class SolanaTransactions extends SolanaModule {
30
+
31
+ private cbkBeforeTxSigned?: (tx: SolanaTx) => Promise<void>;
32
+ /**
33
+ * Callback for sending transaction, returns not null if it was successfully able to send the transaction, and null
34
+ * if the transaction should be sent through other means)
35
+ * @private
36
+ */
37
+ private cbkSendTransaction?: (tx: Buffer, options?: SendOptions) => Promise<string>;
38
+
39
+ /**
40
+ * Sends raw solana transaction, first through the cbkSendTransaction callback (for e.g. sending the transaction
41
+ * to a different specific RPC), the through the Fees handler (for e.g. Jito transaction) and last through the
42
+ * underlying provider's Connection instance (the usual way). Only sends the transaction through one channel.
43
+ *
44
+ * @param data
45
+ * @param options
46
+ * @private
47
+ */
48
+ private async sendRawTransaction(data: Buffer, options?: SendOptions): Promise<string> {
49
+ let result: string | null = null;
50
+ options ??= {};
51
+ options.maxRetries = 0;
52
+ if(this.cbkSendTransaction!=null) result = await this.cbkSendTransaction(data, options);
53
+ if(result==null) result = await this.root.Fees.submitTx(data, options);
54
+ if(result==null) result = await this.connection.sendRawTransaction(data, options);
55
+ // this.logger.debug("sendRawTransaction(): tx sent, signature: "+result);
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Waits for the transaction to confirm by periodically checking the transaction status over HTTP, also
61
+ * re-sends the transaction at regular intervals
62
+ *
63
+ * @param tx solana tx to wait for confirmation for
64
+ * @param finality wait for this finality
65
+ * @param abortSignal signal to abort waiting for tx confirmation
66
+ * @private
67
+ */
68
+ private txConfirmationAndResendWatchdog(
69
+ tx: Transaction,
70
+ finality?: Finality,
71
+ abortSignal?: AbortSignal
72
+ ): Promise<string> {
73
+ if(tx.signature==null) throw new Error("Cannot check confirmation status of tx without signature!");
74
+ const rawTx = tx.serialize();
75
+ const signature = bs58.encode(tx.signature);
76
+ return new Promise((resolve, reject) => {
77
+ let watchdogInterval: any;
78
+ watchdogInterval = setInterval(async () => {
79
+ const result = await this.sendRawTransaction(rawTx, {skipPreflight: true}).catch(
80
+ e => this.logger.error("txConfirmationAndResendWatchdog(): transaction re-sent error: ", e)
81
+ );
82
+ this.logger.debug("txConfirmationAndResendWatchdog(): transaction re-sent: "+result);
83
+
84
+ let status = await this.getTxIdStatus(signature, finality).catch(
85
+ e => this.logger.error("txConfirmationAndResendWatchdog(): get tx id status error: ", e)
86
+ );
87
+ if(status==null || status==="not_found") {
88
+ const blockValidity = await this.connection.isBlockhashValid(tx.recentBlockhash!, {commitment: finality}).catch(
89
+ e => this.logger.error("txConfirmationAndResendWatchdog(): blockhash validity check error: ", e)
90
+ );
91
+ if(!blockValidity) return;
92
+ if(blockValidity.value) return;
93
+ try {
94
+ //One last try to get the txId status
95
+ const statusCheck = await this.getTxIdStatus(signature, finality);
96
+ if(statusCheck === "not_found") reject(new Error("Transaction expired before confirmation, please try again!"));
97
+ status = statusCheck;
98
+ } catch (e) {
99
+ this.logger.error("txConfirmationAndResendWatchdog(): re-check get tx id status error: ", e);
100
+ return;
101
+ }
102
+ }
103
+ if(status==="success") {
104
+ this.logger.info("txConfirmationAndResendWatchdog(): transaction confirmed from HTTP polling, signature: "+signature);
105
+ resolve(signature);
106
+ }
107
+ if(status==="reverted") reject(new TransactionRevertedError("Transaction reverted!"));
108
+ clearInterval(watchdogInterval);
109
+ }, this.retryPolicy?.transactionResendInterval || 3000);
110
+
111
+ if(abortSignal!=null) abortSignal.addEventListener("abort", () => {
112
+ clearInterval(watchdogInterval);
113
+ reject(abortSignal.reason);
114
+ });
115
+ })
116
+ }
117
+
118
+ /**
119
+ * Waits for the transaction to confirm from WS, sometimes the WS rejects even though the transaction was confirmed
120
+ * this therefore also runs an ultimate check on the transaction in case the WS handler rejects, checking if it
121
+ * really was expired
122
+ *
123
+ * @param tx solana tx to wait for confirmation for
124
+ * @param finality wait for this finality
125
+ * @param abortSignal signal to abort waiting for tx confirmation
126
+ * @private
127
+ */
128
+ private async txConfirmFromWebsocket(
129
+ tx: Transaction,
130
+ finality?: Finality,
131
+ abortSignal?: AbortSignal
132
+ ): Promise<string> {
133
+ if(tx.signature==null) throw new Error("Cannot wait for confirmation for tx without signature!");
134
+ const signature = bs58.encode(tx.signature);
135
+
136
+ let result: SignatureResult;
137
+ try {
138
+ result = await new Promise<SignatureResult>((resolve, reject) => {
139
+ let subscriptionId: number | undefined;
140
+
141
+ if(abortSignal!=null) abortSignal.addEventListener("abort", () => {
142
+ if(subscriptionId!=null) this.connection.removeSignatureListener(subscriptionId).catch(e => {
143
+ this.logger.debug("txConfirmFromWebsocket(): remove WS signature confirm listener error: ", e);
144
+ });
145
+ subscriptionId = undefined;
146
+ reject(abortSignal.reason);
147
+ });
148
+
149
+ subscriptionId = this.connection.onSignature(signature, (data) => {
150
+ resolve(data);
151
+ subscriptionId = undefined;
152
+ }, finality);
153
+ });
154
+ this.logger.info("txConfirmFromWebsocket(): transaction confirmed from WS, signature: "+signature);
155
+ } catch (err: any) {
156
+ if(abortSignal!=null && abortSignal.aborted) throw err;
157
+ this.logger.debug("txConfirmFromWebsocket(): transaction rejected from WS, running ultimate check, expiry blockheight: "+tx.lastValidBlockHeight+" signature: "+signature+" error: "+err);
158
+ const status = await tryWithRetries(
159
+ () => this.getTxIdStatus(signature, finality)
160
+ );
161
+ this.logger.info("txConfirmFromWebsocket(): transaction status: "+status+" signature: "+signature);
162
+ if(status==="success") return signature;
163
+ if(status==="reverted") throw new TransactionRevertedError("Transaction reverted!");
164
+ if(err instanceof TransactionExpiredBlockheightExceededError || err.toString().startsWith("TransactionExpiredBlockheightExceededError")) {
165
+ throw new Error("Transaction expired before confirmation, please try again!");
166
+ } else {
167
+ throw err;
168
+ }
169
+ }
170
+ if(result.err!=null) throw new TransactionRevertedError("Transaction reverted!");
171
+ return signature;
172
+ }
173
+
174
+ /**
175
+ * Waits for transaction confirmation using WS subscription and occasional HTTP polling, also re-sends
176
+ * the transaction at regular interval
177
+ *
178
+ * @param tx solana transaction to wait for confirmation for & keep re-sending until it confirms
179
+ * @param abortSignal signal to abort waiting for tx confirmation
180
+ * @param finality wait for specific finality
181
+ * @private
182
+ */
183
+ private async confirmTransaction(tx: Transaction, abortSignal?: AbortSignal, finality?: Finality) {
184
+ const abortController = new AbortController();
185
+ if(abortSignal!=null) {
186
+ abortSignal.throwIfAborted();
187
+ abortSignal.addEventListener("abort", () => {
188
+ abortController.abort(abortSignal.reason);
189
+ });
190
+ }
191
+
192
+ let txSignature: string;
193
+ try {
194
+ txSignature = await Promise.race([
195
+ this.txConfirmationAndResendWatchdog(tx, finality, abortController.signal),
196
+ this.txConfirmFromWebsocket(tx, finality, abortController.signal)
197
+ ]);
198
+ } catch (e) {
199
+ abortController.abort(e);
200
+ throw e;
201
+ }
202
+
203
+ // this.logger.info("confirmTransaction(): transaction confirmed, signature: "+txSignature);
204
+
205
+ abortController.abort();
206
+ }
207
+
208
+ /**
209
+ * Prepares solana transactions, assigns recentBlockhash if needed, applies Phantom hotfix,
210
+ * sets feePayer to ourselves, calls beforeTxSigned callback & signs transaction with provided signers array
211
+ *
212
+ * @param txs
213
+ * @param signer
214
+ */
215
+ public async prepareTransactions(txs: SolanaTx[], signer?: SolanaSigner): Promise<void> {
216
+ if(txs.length===0) return;
217
+ const signerAddress = signer?.getPublicKey() ?? txs[0].tx.feePayer;
218
+ if(signerAddress==null) throw new Error("Cannot get tx sender address!");
219
+
220
+ let latestBlockData: {blockhash: string, lastValidBlockHeight: number} | null = null;
221
+
222
+ for(let tx of txs) {
223
+ if(tx.tx.recentBlockhash==null) {
224
+ if(latestBlockData==null) {
225
+ latestBlockData = await this.connection.getLatestBlockhash("confirmed");
226
+ this.logger.debug("prepareTransactions(): fetched latest block data for transactions," +
227
+ " blockhash: "+latestBlockData.blockhash+" expiry blockheight: "+latestBlockData.lastValidBlockHeight);
228
+ }
229
+ tx.tx.recentBlockhash = latestBlockData.blockhash;
230
+ tx.tx.lastValidBlockHeight = latestBlockData.lastValidBlockHeight;
231
+ }
232
+
233
+ //This is a hotfix for Phantom adding compute unit price instruction on the first position & breaking
234
+ // required instructions order (e.g. btc relay verify needs to be 0th instruction in a tx)
235
+ if((signer==null || signer.keypair==null) && tx.tx.signatures.length===0) {
236
+ const foundIx = tx.tx.instructions.find(ix => ix.programId.equals(ComputeBudgetProgram.programId) && ComputeBudgetInstruction.decodeInstructionType(ix)==="SetComputeUnitPrice")
237
+ if(foundIx==null) tx.tx.instructions.splice(tx.tx.instructions.length-1, 0, ComputeBudgetProgram.setComputeUnitPrice({microLamports: 1}));
238
+ }
239
+
240
+ tx.tx.feePayer = signerAddress;
241
+ if(signer!=null && this.cbkBeforeTxSigned!=null) await this.cbkBeforeTxSigned(tx);
242
+ if(tx.signers!=null && tx.signers.length>0) for(let signer of tx.signers) tx.tx.sign(signer);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Sends out a signed transaction to the RPC
248
+ *
249
+ * @param tx solana tx to send
250
+ * @param options send options to be passed to the RPC
251
+ * @param onBeforePublish a callback called before every transaction is published
252
+ * @private
253
+ */
254
+ private async sendSignedTransaction(tx: Transaction, options?: SendOptions, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string> {
255
+ if(tx.signature==null) throw new Error("Cannot broadcast tx without signature!");
256
+ const signature = bs58.encode(tx.signature);
257
+
258
+ if(onBeforePublish!=null) await onBeforePublish(signature, this.serializeSignedTx(tx));
259
+ const serializedTx = tx.serialize();
260
+ this.logger.debug("sendSignedTransaction(): sending transaction: "+serializedTx.toString("hex")+
261
+ " signature: "+signature);
262
+ const txResult = await tryWithRetries(() => this.sendRawTransaction(serializedTx, options), this.retryPolicy);
263
+ this.logger.info("sendSignedTransaction(): tx sent, signature: "+txResult);
264
+ return txResult;
265
+ }
266
+
267
+ /**
268
+ * Prepares (adds recent blockhash if required, applies Phantom hotfix),
269
+ * signs (all together using signAllTransactions() calls), sends (in parallel or sequentially) &
270
+ * optionally waits for confirmation of a batch of solana transactions
271
+ *
272
+ * @param signer
273
+ * @param _txs
274
+ * @param waitForConfirmation whether to wait for transaction confirmations (this also makes sure the transactions
275
+ * are re-sent at regular intervals)
276
+ * @param abortSignal abort signal to abort waiting for transaction confirmations
277
+ * @param parallel whether the send all the transaction at once in parallel or sequentially (such that transactions
278
+ * are executed in order)
279
+ * @param onBeforePublish a callback called before every transaction is published
280
+ */
281
+ public async sendAndConfirm(signer: SolanaSigner, _txs: SolanaTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal, parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string[]> {
282
+ const options = {
283
+ skipPreflight: true
284
+ };
285
+
286
+ this.logger.debug("sendAndConfirm(): sending transactions, count: "+_txs.length+
287
+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
288
+
289
+ const BATCH_SIZE = 50;
290
+
291
+ const signatures: string[] = [];
292
+ for(let e=0;e<_txs.length;e+=BATCH_SIZE) {
293
+ const txs = _txs.slice(e, e+BATCH_SIZE);
294
+
295
+ await this.prepareTransactions(txs, signer)
296
+ const signedTxs = await signer.wallet.signAllTransactions(txs.map(e => e.tx));
297
+ signedTxs.forEach((tx, index) => {
298
+ const solTx = txs[index];
299
+ tx.lastValidBlockHeight = solTx.tx.lastValidBlockHeight;
300
+ solTx.tx = tx
301
+ });
302
+ this.logger.debug("sendAndConfirm(): sending transaction batch ("+e+".."+(e+50)+"), count: "+txs.length);
303
+
304
+ //For solana we are forced to send txs one-by-one even with parallel, as we cannot determine their order upfront,
305
+ // however e.g. Jito could possibly handle sending a single package of up to 5 txns in order.
306
+ for(let i=0;i<txs.length;i++) {
307
+ const solTx = txs[i];
308
+ const signature = await this.sendSignedTransaction(solTx.tx, options, onBeforePublish);
309
+ const confirmPromise = this.confirmTransaction(solTx.tx, abortSignal, "confirmed");
310
+ //Don't await the last promise when !waitForConfirmation
311
+ if(i<txs.length-1 || e+50<_txs.length || waitForConfirmation) {
312
+ await confirmPromise;
313
+ } else {
314
+ confirmPromise.catch(err => this.logger.error(`sendAndConfirm(): Error while awaiting confirmation of ${signature}: `, err));
315
+ }
316
+ signatures.push(signature);
317
+ }
318
+ }
319
+
320
+ this.logger.info("sendAndConfirm(): sent transactions, count: "+_txs.length+
321
+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
322
+
323
+ return signatures;
324
+ }
325
+
326
+ public async sendSignedAndConfirm(
327
+ signedTxs: SignedSolanaTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal,
328
+ parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>
329
+ ): Promise<string[]> {
330
+ //Verify all txns are properly signed
331
+ signedTxs.forEach(val => {
332
+ const pubkeysSigned: Set<string> = new Set<string>(
333
+ val.signatures.map(val => val.publicKey.toString())
334
+ );
335
+ val.instructions.forEach(ix => {
336
+ ix.keys.forEach(key => {
337
+ if(key.isSigner && !pubkeysSigned.has(key.pubkey.toString()))
338
+ throw new Error("Transaction requires signature by: "+key.pubkey.toString());
339
+ });
340
+ });
341
+ });
342
+
343
+ const options = {
344
+ skipPreflight: true
345
+ };
346
+
347
+ this.logger.debug("sendSignedAndConfirm(): sending transactions, count: "+signedTxs.length+
348
+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
349
+
350
+ const abortController = new AbortController();
351
+ if(abortSignal!=null) {
352
+ abortSignal.throwIfAborted();
353
+ abortSignal.addEventListener("abort", () => abortController.abort(abortSignal.reason));
354
+ }
355
+
356
+ const signatures: string[] = [];
357
+ const promises: Promise<void>[] = [];
358
+ for(let i=0; i<signedTxs.length; i++) {
359
+ const signedTx = signedTxs[i];
360
+ this.logger.debug("sendSignedAndConfirm(): sending transaction "+i+", total count: "+signedTxs.length);
361
+ const signature = await this.sendSignedTransaction(signedTx, options, onBeforePublish);
362
+ if(abortSignal!=null) abortSignal.throwIfAborted();
363
+ const confirmPromise = this.confirmTransaction(signedTx, abortController.signal, "confirmed");
364
+ signatures.push(signature);
365
+ if(!parallel) {
366
+ //Don't await the last one when not wait for confirmations
367
+ if(i<signedTxs.length-1 || waitForConfirmation) {
368
+ await confirmPromise;
369
+ continue;
370
+ }
371
+ } else {
372
+ if(waitForConfirmation) {
373
+ promises.push(confirmPromise.catch(err => {
374
+ this.logger.error(`sendSignedAndConfirm(): Error while awaiting confirmation of ${signature}: `, err);
375
+ abortController.abort(err);
376
+ }));
377
+ continue;
378
+ }
379
+ }
380
+ confirmPromise.catch(err => this.logger.error(`sendSignedAndConfirm(): Error while awaiting confirmation of ${signature}: `, err));
381
+ }
382
+
383
+ abortController.signal.throwIfAborted();
384
+ if(parallel && waitForConfirmation) await Promise.all(promises);
385
+ abortController.signal.throwIfAborted();
386
+
387
+ this.logger.info("sendSignedAndConfirm(): sent transactions, count: "+signedTxs.length+
388
+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
389
+
390
+ return signatures;
391
+ }
392
+
393
+ /**
394
+ * Serializes the solana transaction, saves the transaction, signers & last valid blockheight
395
+ *
396
+ * @param tx
397
+ */
398
+ public serializeUnsignedTx(tx: SolanaTx): string {
399
+ return JSON.stringify({
400
+ tx: tx.tx.serialize({requireAllSignatures: false, verifySignatures: false}).toString("hex"),
401
+ signers: tx.signers.map(e => Buffer.from(e.secretKey).toString("hex")),
402
+ lastValidBlockheight: tx.tx.lastValidBlockHeight
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Serializes the solana transaction
408
+ *
409
+ * @param signedTx
410
+ */
411
+ public serializeSignedTx(signedTx: Transaction): string {
412
+ return signedTx.serialize({requireAllSignatures: false, verifySignatures: false}).toString("hex");
413
+ }
414
+
415
+ /**
416
+ * Deserializes saved solana transaction, extracting the transaction, signers & last valid blockheight
417
+ *
418
+ * @param txData
419
+ */
420
+ public deserializeUnsignedTx(txData: string): SolanaTx {
421
+ const jsonParsed: {
422
+ tx: string,
423
+ signers: string[],
424
+ lastValidBlockheight: number
425
+ } = JSON.parse(txData);
426
+
427
+ const transaction = Transaction.from(Buffer.from(jsonParsed.tx, "hex"));
428
+ transaction.lastValidBlockHeight = jsonParsed.lastValidBlockheight;
429
+
430
+ return {
431
+ tx: transaction,
432
+ signers: jsonParsed.signers.map(e => Keypair.fromSecretKey(Buffer.from(e, "hex"))),
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Deserializes raw solana transaction
438
+ *
439
+ * @param txData
440
+ */
441
+ public deserializeSignedTransaction(txData: string): Transaction {
442
+ return Transaction.from(Buffer.from(txData, "hex"));
443
+ }
444
+
445
+ /**
446
+ * Gets the status of the raw solana transaction, this also checks transaction expiry & can therefore report tx
447
+ * in "pending" status, however pending status doesn't necessarily mean that the transaction was sent (again,
448
+ * no mempool on Solana, cannot check that), this function is preferred against getTxIdStatus
449
+ *
450
+ * @param tx
451
+ */
452
+ public async getTxStatus(tx: string): Promise<"pending" | "success" | "not_found" | "reverted"> {
453
+ const parsedTx: Transaction = this.deserializeSignedTransaction(tx);
454
+ const signature = bs58.encode(parsedTx.signature!);
455
+ const txReceipt = await this.connection.getTransaction(signature, {
456
+ commitment: "confirmed",
457
+ maxSupportedTransactionVersion: 0
458
+ });
459
+ if(txReceipt==null) {
460
+ const {value: isValid} = await this.connection.isBlockhashValid(parsedTx.recentBlockhash!, {commitment: "processed"});
461
+ if(!isValid) return "not_found";
462
+ return "pending";
463
+ }
464
+ if(txReceipt.meta==null) throw new Error(`Cannot read status (meta) of Solana transaction: ${signature}`);
465
+ if(txReceipt.meta.err) return "reverted";
466
+ return "success";
467
+ }
468
+
469
+ /**
470
+ * Gets the status of the solana transaction with a specific txId, this cannot report whether the transaction is
471
+ * "pending" because Solana has no concept of mempool & only confirmed transactions are accessible
472
+ *
473
+ * @param txId
474
+ * @param finality
475
+ */
476
+ public async getTxIdStatus(txId: string, finality?: Finality): Promise<"success" | "not_found" | "reverted"> {
477
+ const txReceipt = await this.connection.getTransaction(txId, {
478
+ commitment: finality || "confirmed",
479
+ maxSupportedTransactionVersion: 0
480
+ });
481
+ if(txReceipt==null) return "not_found";
482
+ if(txReceipt.meta==null) throw new Error(`Cannot read status (meta) of Solana transaction: ${txId}`);
483
+ if(txReceipt.meta.err) return "reverted";
484
+ return "success";
485
+ }
486
+
487
+ public onBeforeTxSigned(callback: (tx: SolanaTx) => Promise<void>): void {
488
+ this.cbkBeforeTxSigned = callback;
489
+ }
490
+
491
+ public offBeforeTxSigned(callback: (tx: SolanaTx) => Promise<void>): boolean {
492
+ delete this.cbkBeforeTxSigned;
493
+ return true;
494
+ }
495
+
496
+ public onSendTransaction(callback: (tx: Buffer, options?: SendOptions) => Promise<string>): void {
497
+ this.cbkSendTransaction = callback;
498
+ }
499
+
500
+ public offSendTransaction(callback: (tx: Buffer, options?: SendOptions) => Promise<string>): boolean {
501
+ delete this.cbkSendTransaction;
502
+ return true;
503
+ }
504
504
  }