@atomiqlabs/chain-solana 7.2.0
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 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +44 -0
- package/dist/solana/SolanaChainType.d.ts +9 -0
- package/dist/solana/SolanaChainType.js +2 -0
- package/dist/solana/base/SolanaAction.d.ts +26 -0
- package/dist/solana/base/SolanaAction.js +99 -0
- package/dist/solana/base/SolanaBase.d.ts +36 -0
- package/dist/solana/base/SolanaBase.js +30 -0
- package/dist/solana/base/SolanaModule.d.ts +14 -0
- package/dist/solana/base/SolanaModule.js +13 -0
- package/dist/solana/base/modules/SolanaAddresses.d.ts +9 -0
- package/dist/solana/base/modules/SolanaAddresses.js +23 -0
- package/dist/solana/base/modules/SolanaBlocks.d.ts +28 -0
- package/dist/solana/base/modules/SolanaBlocks.js +83 -0
- package/dist/solana/base/modules/SolanaEvents.d.ts +25 -0
- package/dist/solana/base/modules/SolanaEvents.js +69 -0
- package/dist/solana/base/modules/SolanaFees.d.ts +121 -0
- package/dist/solana/base/modules/SolanaFees.js +393 -0
- package/dist/solana/base/modules/SolanaSignatures.d.ts +23 -0
- package/dist/solana/base/modules/SolanaSignatures.js +39 -0
- package/dist/solana/base/modules/SolanaSlots.d.ts +31 -0
- package/dist/solana/base/modules/SolanaSlots.js +81 -0
- package/dist/solana/base/modules/SolanaTokens.d.ts +134 -0
- package/dist/solana/base/modules/SolanaTokens.js +269 -0
- package/dist/solana/base/modules/SolanaTransactions.d.ts +124 -0
- package/dist/solana/base/modules/SolanaTransactions.js +354 -0
- package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +229 -0
- package/dist/solana/btcrelay/SolanaBtcRelay.js +477 -0
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +29 -0
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +34 -0
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +46 -0
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +78 -0
- package/dist/solana/btcrelay/program/programIdl.json +671 -0
- package/dist/solana/events/SolanaChainEvents.d.ts +84 -0
- package/dist/solana/events/SolanaChainEvents.js +268 -0
- package/dist/solana/events/SolanaChainEventsBrowser.d.ts +85 -0
- package/dist/solana/events/SolanaChainEventsBrowser.js +202 -0
- package/dist/solana/program/SolanaProgramBase.d.ts +34 -0
- package/dist/solana/program/SolanaProgramBase.js +43 -0
- package/dist/solana/program/modules/SolanaProgramEvents.d.ts +58 -0
- package/dist/solana/program/modules/SolanaProgramEvents.js +114 -0
- package/dist/solana/swaps/SolanaSwapData.d.ts +55 -0
- package/dist/solana/swaps/SolanaSwapData.js +251 -0
- package/dist/solana/swaps/SolanaSwapModule.d.ts +9 -0
- package/dist/solana/swaps/SolanaSwapModule.js +12 -0
- package/dist/solana/swaps/SolanaSwapProgram.d.ts +218 -0
- package/dist/solana/swaps/SolanaSwapProgram.js +523 -0
- package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -0
- package/dist/solana/swaps/SwapTypeEnum.js +42 -0
- package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +94 -0
- package/dist/solana/swaps/modules/SolanaDataAccount.js +255 -0
- package/dist/solana/swaps/modules/SolanaLpVault.d.ts +72 -0
- package/dist/solana/swaps/modules/SolanaLpVault.js +196 -0
- package/dist/solana/swaps/modules/SwapClaim.d.ts +129 -0
- package/dist/solana/swaps/modules/SwapClaim.js +307 -0
- package/dist/solana/swaps/modules/SwapInit.d.ts +212 -0
- package/dist/solana/swaps/modules/SwapInit.js +508 -0
- package/dist/solana/swaps/modules/SwapRefund.d.ts +83 -0
- package/dist/solana/swaps/modules/SwapRefund.js +264 -0
- package/dist/solana/swaps/programIdl.json +945 -0
- package/dist/solana/swaps/programTypes.d.ts +943 -0
- package/dist/solana/swaps/programTypes.js +945 -0
- package/dist/solana/wallet/SolanaKeypairWallet.d.ts +9 -0
- package/dist/solana/wallet/SolanaKeypairWallet.js +33 -0
- package/dist/solana/wallet/SolanaSigner.d.ts +10 -0
- package/dist/solana/wallet/SolanaSigner.js +16 -0
- package/dist/utils/Utils.d.ts +43 -0
- package/dist/utils/Utils.js +143 -0
- package/package.json +40 -0
- package/src/index.ts +35 -0
- package/src/solana/SolanaChainType.ts +20 -0
- package/src/solana/base/SolanaAction.ts +109 -0
- package/src/solana/base/SolanaBase.ts +57 -0
- package/src/solana/base/SolanaModule.ts +21 -0
- package/src/solana/base/modules/SolanaAddresses.ts +22 -0
- package/src/solana/base/modules/SolanaBlocks.ts +79 -0
- package/src/solana/base/modules/SolanaEvents.ts +58 -0
- package/src/solana/base/modules/SolanaFees.ts +445 -0
- package/src/solana/base/modules/SolanaSignatures.ts +40 -0
- package/src/solana/base/modules/SolanaSlots.ts +83 -0
- package/src/solana/base/modules/SolanaTokens.ts +310 -0
- package/src/solana/base/modules/SolanaTransactions.ts +366 -0
- package/src/solana/btcrelay/SolanaBtcRelay.ts +591 -0
- package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +58 -0
- package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +103 -0
- package/src/solana/btcrelay/program/programIdl.json +671 -0
- package/src/solana/events/SolanaChainEvents.ts +286 -0
- package/src/solana/events/SolanaChainEventsBrowser.ts +251 -0
- package/src/solana/program/SolanaProgramBase.ts +77 -0
- package/src/solana/program/modules/SolanaProgramEvents.ts +140 -0
- package/src/solana/swaps/SolanaSwapData.ts +360 -0
- package/src/solana/swaps/SolanaSwapModule.ts +17 -0
- package/src/solana/swaps/SolanaSwapProgram.ts +739 -0
- package/src/solana/swaps/SwapTypeEnum.ts +30 -0
- package/src/solana/swaps/modules/SolanaDataAccount.ts +309 -0
- package/src/solana/swaps/modules/SolanaLpVault.ts +216 -0
- package/src/solana/swaps/modules/SwapClaim.ts +397 -0
- package/src/solana/swaps/modules/SwapInit.ts +621 -0
- package/src/solana/swaps/modules/SwapRefund.ts +316 -0
- package/src/solana/swaps/programIdl.json +945 -0
- package/src/solana/swaps/programTypes.ts +1885 -0
- package/src/solana/wallet/SolanaKeypairWallet.ts +36 -0
- package/src/solana/wallet/SolanaSigner.ts +23 -0
- package/src/utils/Utils.ts +145 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {SolanaModule} from "../SolanaModule";
|
|
2
|
+
import {PublicKey, SystemProgram} from "@solana/web3.js";
|
|
3
|
+
import {
|
|
4
|
+
Account, createAssociatedTokenAccountInstruction,
|
|
5
|
+
createCloseAccountInstruction, createSyncNativeInstruction, createTransferInstruction,
|
|
6
|
+
getAccount, getAssociatedTokenAddress,
|
|
7
|
+
getAssociatedTokenAddressSync,
|
|
8
|
+
TokenAccountNotFoundError
|
|
9
|
+
} from "@solana/spl-token";
|
|
10
|
+
import * as BN from "bn.js";
|
|
11
|
+
import {SolanaTx} from "./SolanaTransactions";
|
|
12
|
+
import {SolanaAction} from "../SolanaAction";
|
|
13
|
+
import {tryWithRetries} from "../../../utils/Utils";
|
|
14
|
+
|
|
15
|
+
export class SolanaTokens extends SolanaModule {
|
|
16
|
+
|
|
17
|
+
public static readonly CUCosts = {
|
|
18
|
+
WRAP_SOL: 10000,
|
|
19
|
+
ATA_CLOSE: 10000,
|
|
20
|
+
ATA_INIT: 40000,
|
|
21
|
+
TRANSFER: 50000,
|
|
22
|
+
TRANSFER_SOL: 5000
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an ATA for a specific public key & token, the ATA creation is paid for by the underlying provider's
|
|
27
|
+
* public key
|
|
28
|
+
*
|
|
29
|
+
* @param signer
|
|
30
|
+
* @param publicKey public key address of the user for which to initiate the ATA
|
|
31
|
+
* @param token token identification for which the ATA should be initialized
|
|
32
|
+
* @param requiredAta optional required ata address to use, if the address doesn't match it returns null
|
|
33
|
+
* @constructor
|
|
34
|
+
*/
|
|
35
|
+
public InitAta(signer: PublicKey, publicKey: PublicKey, token: PublicKey, requiredAta?: PublicKey): SolanaAction | null {
|
|
36
|
+
const ata = getAssociatedTokenAddressSync(token, publicKey);
|
|
37
|
+
if(requiredAta!=null && !ata.equals(requiredAta)) return null;
|
|
38
|
+
return new SolanaAction(
|
|
39
|
+
signer,
|
|
40
|
+
this.root,
|
|
41
|
+
createAssociatedTokenAccountInstruction(
|
|
42
|
+
signer,
|
|
43
|
+
ata,
|
|
44
|
+
publicKey,
|
|
45
|
+
token
|
|
46
|
+
),
|
|
47
|
+
SolanaTokens.CUCosts.ATA_INIT
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Action for wrapping SOL to WSOL for a specific public key
|
|
53
|
+
*
|
|
54
|
+
* @param publicKey public key of the user for which to wrap the SOL
|
|
55
|
+
* @param amount amount of SOL in lamports (smallest unit) to wrap
|
|
56
|
+
* @param initAta whether we should also initialize the ATA before depositing SOL
|
|
57
|
+
* @constructor
|
|
58
|
+
*/
|
|
59
|
+
public Wrap(publicKey: PublicKey, amount: BN, initAta: boolean): SolanaAction {
|
|
60
|
+
const ata = getAssociatedTokenAddressSync(this.WSOL_ADDRESS, publicKey);
|
|
61
|
+
const action = new SolanaAction(publicKey, this.root);
|
|
62
|
+
if(initAta) action.addIx(
|
|
63
|
+
createAssociatedTokenAccountInstruction(publicKey, ata, publicKey, this.WSOL_ADDRESS),
|
|
64
|
+
SolanaTokens.CUCosts.ATA_INIT
|
|
65
|
+
);
|
|
66
|
+
action.addIx(
|
|
67
|
+
SystemProgram.transfer({
|
|
68
|
+
fromPubkey: publicKey,
|
|
69
|
+
toPubkey: ata,
|
|
70
|
+
lamports: BigInt(amount.toString(10))
|
|
71
|
+
}),
|
|
72
|
+
SolanaTokens.CUCosts.WRAP_SOL
|
|
73
|
+
);
|
|
74
|
+
action.addIx(createSyncNativeInstruction(ata));
|
|
75
|
+
return action;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Action for unwrapping WSOL to SOL for a specific public key
|
|
80
|
+
*
|
|
81
|
+
* @param publicKey public key of the user for which to unwrap the sol
|
|
82
|
+
* @constructor
|
|
83
|
+
*/
|
|
84
|
+
public Unwrap(publicKey: PublicKey): SolanaAction {
|
|
85
|
+
const ata = getAssociatedTokenAddressSync(this.WSOL_ADDRESS, publicKey);
|
|
86
|
+
return new SolanaAction(
|
|
87
|
+
publicKey,
|
|
88
|
+
this.root,
|
|
89
|
+
createCloseAccountInstruction(ata, publicKey, publicKey),
|
|
90
|
+
SolanaTokens.CUCosts.ATA_CLOSE
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public readonly WSOL_ADDRESS = new PublicKey("So11111111111111111111111111111111111111112");
|
|
95
|
+
public readonly SPL_ATA_RENT_EXEMPT = 2039280;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Action for transferring the native SOL token, uses provider's public key as a sender
|
|
99
|
+
*
|
|
100
|
+
* @param signer
|
|
101
|
+
* @param recipient
|
|
102
|
+
* @param amount
|
|
103
|
+
* @constructor
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
private SolTransfer(signer: PublicKey, recipient: PublicKey, amount: BN): SolanaAction {
|
|
107
|
+
return new SolanaAction(signer, this.root,
|
|
108
|
+
SystemProgram.transfer({
|
|
109
|
+
fromPubkey: signer,
|
|
110
|
+
toPubkey: recipient,
|
|
111
|
+
lamports: BigInt(amount.toString(10))
|
|
112
|
+
}),
|
|
113
|
+
SolanaTokens.CUCosts.TRANSFER_SOL
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Action for transferring the SPL token, uses provider's public key as a sender
|
|
119
|
+
*
|
|
120
|
+
* @param signer
|
|
121
|
+
* @param recipient
|
|
122
|
+
* @param token
|
|
123
|
+
* @param amount
|
|
124
|
+
* @constructor
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
private Transfer(signer: PublicKey, recipient: PublicKey, token: PublicKey, amount: BN): SolanaAction {
|
|
128
|
+
const srcAta = getAssociatedTokenAddressSync(token, signer, false)
|
|
129
|
+
const dstAta = getAssociatedTokenAddressSync(token, recipient, false);
|
|
130
|
+
return new SolanaAction(signer, this.root,
|
|
131
|
+
createTransferInstruction(
|
|
132
|
+
srcAta,
|
|
133
|
+
dstAta,
|
|
134
|
+
signer,
|
|
135
|
+
BigInt(amount.toString(10))
|
|
136
|
+
),
|
|
137
|
+
SolanaTokens.CUCosts.TRANSFER
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Creates transactions for sending SOL (the native token)
|
|
143
|
+
*
|
|
144
|
+
* @param signer
|
|
145
|
+
* @param amount amount of the SOL in lamports (smallest unit) to send
|
|
146
|
+
* @param recipient recipient's address
|
|
147
|
+
* @param feeRate fee rate to use for the transactions
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
private async txsTransferSol(signer: PublicKey, amount: BN, recipient: PublicKey, feeRate?: string): Promise<SolanaTx[]> {
|
|
151
|
+
const wsolAta = getAssociatedTokenAddressSync(this.WSOL_ADDRESS, signer, false);
|
|
152
|
+
|
|
153
|
+
const shouldUnwrap = await this.ataExists(wsolAta);
|
|
154
|
+
const action = new SolanaAction(signer, this.root);
|
|
155
|
+
if(shouldUnwrap) {
|
|
156
|
+
feeRate = feeRate || await this.root.Fees.getFeeRate([signer, recipient, wsolAta]);
|
|
157
|
+
action.add(this.Unwrap(signer));
|
|
158
|
+
} else {
|
|
159
|
+
feeRate = feeRate || await this.root.Fees.getFeeRate([signer, recipient]);
|
|
160
|
+
}
|
|
161
|
+
action.add(this.SolTransfer(signer, recipient, amount));
|
|
162
|
+
|
|
163
|
+
this.logger.debug("txsTransferSol(): transfer native solana TX created, recipient: "+recipient.toString()+
|
|
164
|
+
" amount: "+amount.toString(10)+" unwrapping: "+shouldUnwrap);
|
|
165
|
+
|
|
166
|
+
return [await action.tx(feeRate)];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Creates transactions for sending the over the tokens
|
|
171
|
+
*
|
|
172
|
+
* @param signer
|
|
173
|
+
* @param token token to send
|
|
174
|
+
* @param amount amount of the token to send
|
|
175
|
+
* @param recipient recipient's address
|
|
176
|
+
* @param feeRate fee rate to use for the transactions
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
private async txsTransferTokens(signer: PublicKey, token: PublicKey, amount: BN, recipient: PublicKey, feeRate?: string) {
|
|
180
|
+
const srcAta = await getAssociatedTokenAddress(token, signer);
|
|
181
|
+
const dstAta = getAssociatedTokenAddressSync(token, recipient, false);
|
|
182
|
+
|
|
183
|
+
feeRate = feeRate || await this.root.Fees.getFeeRate([signer, srcAta, dstAta]);
|
|
184
|
+
|
|
185
|
+
const initAta = !await this.ataExists(dstAta);
|
|
186
|
+
const action = new SolanaAction(signer, this.root);
|
|
187
|
+
if(initAta) {
|
|
188
|
+
action.add(this.InitAta(signer, recipient, token));
|
|
189
|
+
}
|
|
190
|
+
action.add(this.Transfer(signer, recipient, token, amount));
|
|
191
|
+
|
|
192
|
+
this.logger.debug("txsTransferTokens(): transfer TX created, recipient: "+recipient.toString()+
|
|
193
|
+
" token: "+token.toString()+ " amount: "+amount.toString(10)+" initAta: "+initAta);
|
|
194
|
+
|
|
195
|
+
return [await action.tx(feeRate)];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
///////////////////
|
|
199
|
+
//// Tokens
|
|
200
|
+
/**
|
|
201
|
+
* Checks if the provided string is a valid solana token
|
|
202
|
+
*
|
|
203
|
+
* @param token
|
|
204
|
+
*/
|
|
205
|
+
public isValidToken(token: string) {
|
|
206
|
+
try {
|
|
207
|
+
new PublicKey(token);
|
|
208
|
+
return true;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns the specific ATA or null if it doesn't exist
|
|
216
|
+
*
|
|
217
|
+
* @param ata
|
|
218
|
+
*/
|
|
219
|
+
public getATAOrNull(ata: PublicKey): Promise<Account | null> {
|
|
220
|
+
return getAccount(this.connection, ata).catch(e => {
|
|
221
|
+
if(e instanceof TokenAccountNotFoundError) return null;
|
|
222
|
+
throw e;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Checks whether the specific ATA exists, uses tryWithRetries so retries on failure
|
|
228
|
+
*
|
|
229
|
+
* @param ata
|
|
230
|
+
*/
|
|
231
|
+
public async ataExists(ata: PublicKey) {
|
|
232
|
+
const account = await tryWithRetries<Account>(
|
|
233
|
+
() => this.getATAOrNull(ata),
|
|
234
|
+
this.retryPolicy
|
|
235
|
+
);
|
|
236
|
+
return account!=null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Returns the rent exempt deposit required to initiate the ATA
|
|
241
|
+
*/
|
|
242
|
+
public getATARentExemptLamports(): Promise<BN> {
|
|
243
|
+
return Promise.resolve(new BN(this.SPL_ATA_RENT_EXEMPT));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Returns the token balance of the public key
|
|
248
|
+
*
|
|
249
|
+
* @param publicKey
|
|
250
|
+
* @param token
|
|
251
|
+
*/
|
|
252
|
+
public async getTokenBalance(publicKey: PublicKey, token: PublicKey) {
|
|
253
|
+
const ata: PublicKey = getAssociatedTokenAddressSync(token, publicKey);
|
|
254
|
+
const [ataAccount, balance] = await Promise.all<[Promise<Account>, Promise<number>]>([
|
|
255
|
+
this.getATAOrNull(ata),
|
|
256
|
+
(token!=null && token.equals(this.WSOL_ADDRESS)) ? this.connection.getBalance(publicKey) : Promise.resolve(null)
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
let ataExists: boolean = ataAccount!=null;
|
|
260
|
+
let sum: BN = new BN(0);
|
|
261
|
+
if(ataExists) {
|
|
262
|
+
sum = sum.add(new BN(ataAccount.amount.toString()));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if(balance!=null) {
|
|
266
|
+
let balanceLamports: BN = new BN(balance);
|
|
267
|
+
if(!ataExists) balanceLamports = balanceLamports.sub(await this.getATARentExemptLamports());
|
|
268
|
+
if(!balanceLamports.isNeg()) sum = sum.add(balanceLamports);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.logger.debug("getTokenBalance(): token balance fetched, token: "+token.toString()+
|
|
272
|
+
" address: "+publicKey.toString()+" amount: "+sum.toString());
|
|
273
|
+
|
|
274
|
+
return sum;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Returns the native currency address, we use WSOL address as placeholder for SOL
|
|
279
|
+
*/
|
|
280
|
+
public getNativeCurrencyAddress(): PublicKey {
|
|
281
|
+
return this.WSOL_ADDRESS;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parses string base58 representation of the token address to a PublicKey object
|
|
286
|
+
* @param address
|
|
287
|
+
*/
|
|
288
|
+
public toTokenAddress(address: string): PublicKey {
|
|
289
|
+
return new PublicKey(address);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
///////////////////
|
|
293
|
+
//// Transfers
|
|
294
|
+
/**
|
|
295
|
+
* Create transactions for sending a specific token to a destination address
|
|
296
|
+
*
|
|
297
|
+
* @param signer
|
|
298
|
+
* @param token token to use for the transfer
|
|
299
|
+
* @param amount amount of token in base units to transfer
|
|
300
|
+
* @param dstAddress destination address of the recipient
|
|
301
|
+
* @param feeRate fee rate to use for the transaction
|
|
302
|
+
*/
|
|
303
|
+
public txsTransfer(signer:PublicKey, token: PublicKey, amount: BN, dstAddress: PublicKey, feeRate?: string): Promise<SolanaTx[]> {
|
|
304
|
+
if(this.WSOL_ADDRESS.equals(token)) {
|
|
305
|
+
return this.txsTransferSol(signer, amount, dstAddress, feeRate);
|
|
306
|
+
}
|
|
307
|
+
return this.txsTransferTokens(signer, token, amount, dstAddress, feeRate);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComputeBudgetInstruction,
|
|
3
|
+
ComputeBudgetProgram, Finality, Keypair, RpcResponseAndContext,
|
|
4
|
+
SendOptions, SignatureResult, Signer, Transaction,
|
|
5
|
+
TransactionExpiredBlockheightExceededError
|
|
6
|
+
} from "@solana/web3.js";
|
|
7
|
+
import {SolanaModule} from "../SolanaModule";
|
|
8
|
+
import * as bs58 from "bs58";
|
|
9
|
+
import {tryWithRetries} from "../../../utils/Utils";
|
|
10
|
+
import {Buffer} from "buffer";
|
|
11
|
+
import {SolanaSigner} from "../../wallet/SolanaSigner";
|
|
12
|
+
|
|
13
|
+
export type SolanaTx = {tx: Transaction, signers: Signer[]};
|
|
14
|
+
|
|
15
|
+
export class SolanaTransactions extends SolanaModule {
|
|
16
|
+
|
|
17
|
+
private cbkBeforeTxSigned: (tx: SolanaTx) => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Callback for sending transaction, returns not null if it was successfully able to send the transaction, and null
|
|
20
|
+
* if the transaction should be sent through other means)
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
private cbkSendTransaction: (tx: Buffer, options?: SendOptions) => Promise<string>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sends raw solana transaction, first through the cbkSendTransaction callback (for e.g. sending the transaction
|
|
27
|
+
* to a different specific RPC), the through the Fees handler (for e.g. Jito transaction) and last through the
|
|
28
|
+
* underlying provider's Connection instance (the usual way). Only sends the transaction through one channel.
|
|
29
|
+
*
|
|
30
|
+
* @param data
|
|
31
|
+
* @param options
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
private async sendRawTransaction(data: Buffer, options?: SendOptions): Promise<string> {
|
|
35
|
+
let result: string = null;
|
|
36
|
+
options ??= {};
|
|
37
|
+
options.maxRetries = 0;
|
|
38
|
+
if(this.cbkSendTransaction!=null) result = await this.cbkSendTransaction(data, options);
|
|
39
|
+
if(result==null) result = await this.root.Fees.submitTx(data, options);
|
|
40
|
+
if(result==null) result = await this.connection.sendRawTransaction(data, options);
|
|
41
|
+
// this.logger.debug("sendRawTransaction(): tx sent, signature: "+result);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Waits for the transaction to confirm by periodically checking the transaction status over HTTP, also
|
|
47
|
+
* re-sends the transaction at regular intervals
|
|
48
|
+
*
|
|
49
|
+
* @param solanaTx solana tx to wait for confirmation for
|
|
50
|
+
* @param finality wait for this finality
|
|
51
|
+
* @param abortSignal signal to abort waiting for tx confirmation
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
private txConfirmationAndResendWatchdog(
|
|
55
|
+
solanaTx: SolanaTx,
|
|
56
|
+
finality?: Finality,
|
|
57
|
+
abortSignal?: AbortSignal
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
const rawTx = solanaTx.tx.serialize();
|
|
60
|
+
const signature = bs58.encode(solanaTx.tx.signature);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
let watchdogInterval: NodeJS.Timer;
|
|
63
|
+
watchdogInterval = setInterval(async () => {
|
|
64
|
+
const result = await this.sendRawTransaction(rawTx, {skipPreflight: true}).catch(
|
|
65
|
+
e => this.logger.error("txConfirmationAndResendWatchdog(): transaction re-sent error: ", e)
|
|
66
|
+
);
|
|
67
|
+
this.logger.debug("txConfirmationAndResendWatchdog(): transaction re-sent: "+result);
|
|
68
|
+
|
|
69
|
+
const status = await this.getTxIdStatus(signature, finality).catch(
|
|
70
|
+
e => this.logger.error("txConfirmationAndResendWatchdog(): get tx id status error: ", e)
|
|
71
|
+
);
|
|
72
|
+
if(status==null || status==="not_found") return;
|
|
73
|
+
if(status==="success") {
|
|
74
|
+
this.logger.info("txConfirmationAndResendWatchdog(): transaction confirmed from HTTP polling, signature: "+signature);
|
|
75
|
+
resolve(signature);
|
|
76
|
+
}
|
|
77
|
+
if(status==="reverted") reject(new Error("Transaction reverted!"));
|
|
78
|
+
clearInterval(watchdogInterval);
|
|
79
|
+
}, this.retryPolicy?.transactionResendInterval || 3000);
|
|
80
|
+
|
|
81
|
+
if(abortSignal!=null) abortSignal.addEventListener("abort", () => {
|
|
82
|
+
clearInterval(watchdogInterval);
|
|
83
|
+
reject(abortSignal.reason);
|
|
84
|
+
});
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Waits for the transaction to confirm from WS, sometimes the WS rejects even though the transaction was confirmed
|
|
90
|
+
* this therefore also runs an ultimate check on the transaction in case the WS handler rejects, checking if it
|
|
91
|
+
* really was expired
|
|
92
|
+
*
|
|
93
|
+
* @param solanaTx solana tx to wait for confirmation for
|
|
94
|
+
* @param finality wait for this finality
|
|
95
|
+
* @param abortSignal signal to abort waiting for tx confirmation
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
private async txConfirmFromWebsocket(
|
|
99
|
+
solanaTx: SolanaTx,
|
|
100
|
+
finality?: Finality,
|
|
101
|
+
abortSignal?: AbortSignal
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
const signature = bs58.encode(solanaTx.tx.signature);
|
|
104
|
+
|
|
105
|
+
let result: RpcResponseAndContext<SignatureResult>;
|
|
106
|
+
try {
|
|
107
|
+
result = await this.connection.confirmTransaction({
|
|
108
|
+
signature: signature,
|
|
109
|
+
blockhash: solanaTx.tx.recentBlockhash,
|
|
110
|
+
lastValidBlockHeight: solanaTx.tx.lastValidBlockHeight,
|
|
111
|
+
abortSignal
|
|
112
|
+
}, finality);
|
|
113
|
+
this.logger.info("txConfirmFromWebsocket(): transaction confirmed from WS, signature: "+signature);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if(abortSignal!=null && abortSignal.aborted) throw err;
|
|
116
|
+
this.logger.debug("txConfirmFromWebsocket(): transaction rejected from WS, running ultimate check, expiry blockheight: "+solanaTx.tx.lastValidBlockHeight+" signature: "+signature+" error: "+err);
|
|
117
|
+
const status = await tryWithRetries(
|
|
118
|
+
() => this.getTxIdStatus(signature, finality)
|
|
119
|
+
);
|
|
120
|
+
this.logger.info("txConfirmFromWebsocket(): transaction status: "+status+" signature: "+signature);
|
|
121
|
+
if(status==="success") return signature;
|
|
122
|
+
if(status==="reverted") throw new Error("Transaction reverted!");
|
|
123
|
+
if(err instanceof TransactionExpiredBlockheightExceededError || err.toString().startsWith("TransactionExpiredBlockheightExceededError")) {
|
|
124
|
+
throw new Error("Transaction expired before confirmation, please try again!");
|
|
125
|
+
} else {
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if(result.value.err!=null) throw new Error("Transaction reverted!");
|
|
130
|
+
return signature;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Waits for transaction confirmation using WS subscription and occasional HTTP polling, also re-sends
|
|
135
|
+
* the transaction at regular interval
|
|
136
|
+
*
|
|
137
|
+
* @param solanaTx solana transaction to wait for confirmation for & keep re-sending until it confirms
|
|
138
|
+
* @param abortSignal signal to abort waiting for tx confirmation
|
|
139
|
+
* @param finality wait for specific finality
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
private async confirmTransaction(solanaTx: SolanaTx, abortSignal?: AbortSignal, finality?: Finality) {
|
|
143
|
+
const abortController = new AbortController();
|
|
144
|
+
if(abortSignal!=null) abortSignal.addEventListener("abort", () => {
|
|
145
|
+
abortController.abort();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
let txSignature: string;
|
|
149
|
+
try {
|
|
150
|
+
txSignature = await Promise.race([
|
|
151
|
+
this.txConfirmationAndResendWatchdog(solanaTx, finality, abortController.signal),
|
|
152
|
+
this.txConfirmFromWebsocket(solanaTx, finality, abortController.signal)
|
|
153
|
+
]);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
abortController.abort(e);
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// this.logger.info("confirmTransaction(): transaction confirmed, signature: "+txSignature);
|
|
160
|
+
|
|
161
|
+
abortController.abort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Prepares solana transactions, assigns recentBlockhash if needed, applies Phantom hotfix,
|
|
166
|
+
* sets feePayer to ourselves, calls beforeTxSigned callback & signs transaction with provided signers array
|
|
167
|
+
*
|
|
168
|
+
* @param signer
|
|
169
|
+
* @param txs
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
private async prepareTransactions(signer: SolanaSigner, txs: SolanaTx[]): Promise<void> {
|
|
173
|
+
let latestBlockData: {blockhash: string, lastValidBlockHeight: number} = null;
|
|
174
|
+
|
|
175
|
+
for(let tx of txs) {
|
|
176
|
+
if(tx.tx.recentBlockhash==null) {
|
|
177
|
+
if(latestBlockData==null) {
|
|
178
|
+
latestBlockData = await tryWithRetries(
|
|
179
|
+
() => this.connection.getLatestBlockhash("confirmed"),
|
|
180
|
+
this.retryPolicy
|
|
181
|
+
);
|
|
182
|
+
this.logger.debug("prepareTransactions(): fetched latest block data for transactions," +
|
|
183
|
+
" blockhash: "+latestBlockData.blockhash+" expiry blockheight: "+latestBlockData.lastValidBlockHeight);
|
|
184
|
+
}
|
|
185
|
+
tx.tx.recentBlockhash = latestBlockData.blockhash;
|
|
186
|
+
tx.tx.lastValidBlockHeight = latestBlockData.lastValidBlockHeight;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
//This is a hotfix for Phantom adding compute unit price instruction on the first position & breaking
|
|
190
|
+
// required instructions order (e.g. btc relay verify needs to be 0th instruction in a tx)
|
|
191
|
+
if(signer.keypair==null && tx.tx.signatures.length===0) {
|
|
192
|
+
const foundIx = tx.tx.instructions.find(ix => ix.programId.equals(ComputeBudgetProgram.programId) && ComputeBudgetInstruction.decodeInstructionType(ix)==="SetComputeUnitPrice")
|
|
193
|
+
if(foundIx==null) tx.tx.instructions.splice(tx.tx.instructions.length-1, 0, ComputeBudgetProgram.setComputeUnitPrice({microLamports: 1}));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
tx.tx.feePayer = signer.getPublicKey();
|
|
197
|
+
if(this.cbkBeforeTxSigned!=null) await this.cbkBeforeTxSigned(tx);
|
|
198
|
+
if(tx.signers!=null && tx.signers.length>0) for(let signer of tx.signers) tx.tx.sign(signer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Sends out a signed transaction to the RPC
|
|
204
|
+
*
|
|
205
|
+
* @param solTx solana tx to send
|
|
206
|
+
* @param options send options to be passed to the RPC
|
|
207
|
+
* @param onBeforePublish a callback called before every transaction is published
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
private async sendSignedTransaction(solTx: SolanaTx, options?: SendOptions, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string> {
|
|
211
|
+
if(onBeforePublish!=null) await onBeforePublish(bs58.encode(solTx.tx.signature), await this.serializeTx(solTx));
|
|
212
|
+
const serializedTx = solTx.tx.serialize();
|
|
213
|
+
this.logger.debug("sendSignedTransaction(): sending transaction: "+serializedTx.toString("hex")+
|
|
214
|
+
" signature: "+bs58.encode(solTx.tx.signature));
|
|
215
|
+
const txResult = await tryWithRetries(() => this.sendRawTransaction(serializedTx, options), this.retryPolicy);
|
|
216
|
+
this.logger.info("sendSignedTransaction(): tx sent, signature: "+txResult);
|
|
217
|
+
return txResult;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Prepares (adds recent blockhash if required, applies Phantom hotfix),
|
|
222
|
+
* signs (all together using signAllTransactions() calls), sends (in parallel or sequentially) &
|
|
223
|
+
* optionally waits for confirmation of a batch of solana transactions
|
|
224
|
+
*
|
|
225
|
+
* @param signer
|
|
226
|
+
* @param txs transactions to send
|
|
227
|
+
* @param waitForConfirmation whether to wait for transaction confirmations (this also makes sure the transactions
|
|
228
|
+
* are re-sent at regular intervals)
|
|
229
|
+
* @param abortSignal abort signal to abort waiting for transaction confirmations
|
|
230
|
+
* @param parallel whether the send all the transaction at once in parallel or sequentially (such that transactions
|
|
231
|
+
* are executed in order)
|
|
232
|
+
* @param onBeforePublish a callback called before every transaction is published
|
|
233
|
+
*/
|
|
234
|
+
public async sendAndConfirm(signer: SolanaSigner, txs: SolanaTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal, parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string[]> {
|
|
235
|
+
await this.prepareTransactions(signer, txs)
|
|
236
|
+
const signedTxs = await signer.wallet.signAllTransactions(txs.map(e => e.tx));
|
|
237
|
+
signedTxs.forEach((tx, index) => {
|
|
238
|
+
const solTx = txs[index];
|
|
239
|
+
tx.lastValidBlockHeight = solTx.tx.lastValidBlockHeight;
|
|
240
|
+
solTx.tx = tx
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const options = {
|
|
244
|
+
skipPreflight: true
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
this.logger.debug("sendAndConfirm(): sending transactions, count: "+txs.length+
|
|
248
|
+
" waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
|
|
249
|
+
|
|
250
|
+
const signatures: string[] = [];
|
|
251
|
+
if(parallel) {
|
|
252
|
+
const promises: Promise<void>[] = [];
|
|
253
|
+
for(let solTx of txs) {
|
|
254
|
+
const signature = await this.sendSignedTransaction(solTx, options, onBeforePublish);
|
|
255
|
+
if(waitForConfirmation) promises.push(this.confirmTransaction(solTx, abortSignal, "confirmed"));
|
|
256
|
+
signatures.push(signature);
|
|
257
|
+
}
|
|
258
|
+
if(promises.length>0) await Promise.all(promises);
|
|
259
|
+
} else {
|
|
260
|
+
for(let i=0;i<txs.length;i++) {
|
|
261
|
+
const solTx = txs[i];
|
|
262
|
+
const signature = await this.sendSignedTransaction(solTx, options, onBeforePublish);
|
|
263
|
+
const confirmPromise = this.confirmTransaction(solTx, abortSignal, "confirmed");
|
|
264
|
+
//Don't await the last promise when !waitForConfirmation
|
|
265
|
+
if(i<txs.length-1 || waitForConfirmation) await confirmPromise;
|
|
266
|
+
signatures.push(signature);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
this.logger.info("sendAndConfirm(): sent transactions, count: "+txs.length+
|
|
271
|
+
" waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel);
|
|
272
|
+
|
|
273
|
+
return signatures;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Serializes the solana transaction, saves the transaction, signers & last valid blockheight
|
|
278
|
+
*
|
|
279
|
+
* @param tx
|
|
280
|
+
*/
|
|
281
|
+
public serializeTx(tx: SolanaTx): Promise<string> {
|
|
282
|
+
return Promise.resolve(JSON.stringify({
|
|
283
|
+
tx: tx.tx.serialize().toString("hex"),
|
|
284
|
+
signers: tx.signers.map(e => Buffer.from(e.secretKey).toString("hex")),
|
|
285
|
+
lastValidBlockheight: tx.tx.lastValidBlockHeight
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Deserializes saved solana transaction, extracting the transaction, signers & last valid blockheight
|
|
291
|
+
*
|
|
292
|
+
* @param txData
|
|
293
|
+
*/
|
|
294
|
+
public deserializeTx(txData: string): Promise<SolanaTx> {
|
|
295
|
+
const jsonParsed: {
|
|
296
|
+
tx: string,
|
|
297
|
+
signers: string[],
|
|
298
|
+
lastValidBlockheight: number
|
|
299
|
+
} = JSON.parse(txData);
|
|
300
|
+
|
|
301
|
+
const transaction = Transaction.from(Buffer.from(jsonParsed.tx, "hex"));
|
|
302
|
+
transaction.lastValidBlockHeight = jsonParsed.lastValidBlockheight;
|
|
303
|
+
|
|
304
|
+
return Promise.resolve({
|
|
305
|
+
tx: transaction,
|
|
306
|
+
signers: jsonParsed.signers.map(e => Keypair.fromSecretKey(Buffer.from(e, "hex"))),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Gets the status of the raw solana transaction, this also checks transaction expiry & can therefore report tx
|
|
312
|
+
* in "pending" status, however pending status doesn't necessarily mean that the transaction was sent (again,
|
|
313
|
+
* no mempool on Solana, cannot check that), this function is preferred against getTxIdStatus
|
|
314
|
+
*
|
|
315
|
+
* @param tx
|
|
316
|
+
*/
|
|
317
|
+
public async getTxStatus(tx: string): Promise<"pending" | "success" | "not_found" | "reverted"> {
|
|
318
|
+
const parsedTx: SolanaTx = await this.deserializeTx(tx);
|
|
319
|
+
const txReceipt = await this.connection.getTransaction(bs58.encode(parsedTx.tx.signature), {
|
|
320
|
+
commitment: "confirmed",
|
|
321
|
+
maxSupportedTransactionVersion: 0
|
|
322
|
+
});
|
|
323
|
+
if(txReceipt==null) {
|
|
324
|
+
const currentBlockheight = await this.connection.getBlockHeight("processed");
|
|
325
|
+
if(currentBlockheight>parsedTx.tx.lastValidBlockHeight) return "not_found";
|
|
326
|
+
return "pending";
|
|
327
|
+
}
|
|
328
|
+
if(txReceipt.meta.err) return "reverted";
|
|
329
|
+
return "success";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Gets the status of the solana transaction with a specific txId, this cannot report whether the transaction is
|
|
334
|
+
* "pending" because Solana has no concept of mempool & only confirmed transactions are accessible
|
|
335
|
+
*
|
|
336
|
+
* @param txId
|
|
337
|
+
* @param finality
|
|
338
|
+
*/
|
|
339
|
+
public async getTxIdStatus(txId: string, finality?: Finality): Promise<"success" | "not_found" | "reverted"> {
|
|
340
|
+
const txReceipt = await this.connection.getTransaction(txId, {
|
|
341
|
+
commitment: finality || "confirmed",
|
|
342
|
+
maxSupportedTransactionVersion: 0
|
|
343
|
+
});
|
|
344
|
+
if(txReceipt==null) return "not_found";
|
|
345
|
+
if(txReceipt.meta.err) return "reverted";
|
|
346
|
+
return "success";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public onBeforeTxSigned(callback: (tx: SolanaTx) => Promise<void>): void {
|
|
350
|
+
this.cbkBeforeTxSigned = callback;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
public offBeforeTxSigned(callback: (tx: SolanaTx) => Promise<void>): boolean {
|
|
354
|
+
this.cbkBeforeTxSigned = null;
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
public onSendTransaction(callback: (tx: Buffer, options?: SendOptions) => Promise<string>): void {
|
|
359
|
+
this.cbkSendTransaction = callback;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public offSendTransaction(callback: (tx: Buffer, options?: SendOptions) => Promise<string>): boolean {
|
|
363
|
+
this.cbkSendTransaction = null;
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
}
|