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