@atomiqlabs/chain-solana 12.0.11 → 12.0.13

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 (114) hide show
  1. package/LICENSE +201 -201
  2. package/dist/index.d.ts +29 -29
  3. package/dist/index.js +45 -45
  4. package/dist/solana/SolanaChainType.d.ts +11 -11
  5. package/dist/solana/SolanaChainType.js +2 -2
  6. package/dist/solana/SolanaChains.d.ts +20 -20
  7. package/dist/solana/SolanaChains.js +25 -25
  8. package/dist/solana/SolanaInitializer.d.ts +18 -18
  9. package/dist/solana/SolanaInitializer.js +63 -63
  10. package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +228 -228
  11. package/dist/solana/btcrelay/SolanaBtcRelay.js +441 -441
  12. package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +29 -29
  13. package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +34 -34
  14. package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +46 -46
  15. package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +78 -78
  16. package/dist/solana/btcrelay/program/programIdl.json +671 -671
  17. package/dist/solana/chain/SolanaAction.d.ts +26 -26
  18. package/dist/solana/chain/SolanaAction.js +86 -86
  19. package/dist/solana/chain/SolanaChainInterface.d.ts +65 -65
  20. package/dist/solana/chain/SolanaChainInterface.js +125 -125
  21. package/dist/solana/chain/SolanaModule.d.ts +14 -14
  22. package/dist/solana/chain/SolanaModule.js +13 -13
  23. package/dist/solana/chain/modules/SolanaAddresses.d.ts +8 -8
  24. package/dist/solana/chain/modules/SolanaAddresses.js +22 -22
  25. package/dist/solana/chain/modules/SolanaBlocks.d.ts +28 -28
  26. package/dist/solana/chain/modules/SolanaBlocks.js +72 -72
  27. package/dist/solana/chain/modules/SolanaEvents.d.ts +68 -68
  28. package/dist/solana/chain/modules/SolanaEvents.js +225 -225
  29. package/dist/solana/chain/modules/SolanaFees.d.ts +121 -121
  30. package/dist/solana/chain/modules/SolanaFees.js +379 -379
  31. package/dist/solana/chain/modules/SolanaSignatures.d.ts +23 -23
  32. package/dist/solana/chain/modules/SolanaSignatures.js +39 -39
  33. package/dist/solana/chain/modules/SolanaSlots.d.ts +31 -31
  34. package/dist/solana/chain/modules/SolanaSlots.js +68 -68
  35. package/dist/solana/chain/modules/SolanaTokens.d.ts +136 -136
  36. package/dist/solana/chain/modules/SolanaTokens.js +248 -248
  37. package/dist/solana/chain/modules/SolanaTransactions.d.ts +124 -124
  38. package/dist/solana/chain/modules/SolanaTransactions.js +323 -323
  39. package/dist/solana/events/SolanaChainEvents.d.ts +88 -88
  40. package/dist/solana/events/SolanaChainEvents.js +256 -256
  41. package/dist/solana/events/SolanaChainEventsBrowser.d.ts +75 -75
  42. package/dist/solana/events/SolanaChainEventsBrowser.js +172 -172
  43. package/dist/solana/program/SolanaProgramBase.d.ts +40 -40
  44. package/dist/solana/program/SolanaProgramBase.js +43 -43
  45. package/dist/solana/program/SolanaProgramModule.d.ts +8 -8
  46. package/dist/solana/program/SolanaProgramModule.js +11 -11
  47. package/dist/solana/program/modules/SolanaProgramEvents.d.ts +53 -53
  48. package/dist/solana/program/modules/SolanaProgramEvents.js +114 -114
  49. package/dist/solana/swaps/SolanaSwapData.d.ts +71 -71
  50. package/dist/solana/swaps/SolanaSwapData.js +292 -292
  51. package/dist/solana/swaps/SolanaSwapModule.d.ts +10 -10
  52. package/dist/solana/swaps/SolanaSwapModule.js +11 -11
  53. package/dist/solana/swaps/SolanaSwapProgram.d.ts +224 -224
  54. package/dist/solana/swaps/SolanaSwapProgram.js +570 -567
  55. package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -11
  56. package/dist/solana/swaps/SwapTypeEnum.js +42 -42
  57. package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +94 -94
  58. package/dist/solana/swaps/modules/SolanaDataAccount.js +231 -231
  59. package/dist/solana/swaps/modules/SolanaLpVault.d.ts +71 -71
  60. package/dist/solana/swaps/modules/SolanaLpVault.js +173 -173
  61. package/dist/solana/swaps/modules/SwapClaim.d.ts +129 -129
  62. package/dist/solana/swaps/modules/SwapClaim.js +291 -291
  63. package/dist/solana/swaps/modules/SwapInit.d.ts +217 -217
  64. package/dist/solana/swaps/modules/SwapInit.js +519 -519
  65. package/dist/solana/swaps/modules/SwapRefund.d.ts +82 -82
  66. package/dist/solana/swaps/modules/SwapRefund.js +262 -262
  67. package/dist/solana/swaps/programIdl.json +945 -945
  68. package/dist/solana/swaps/programTypes.d.ts +943 -943
  69. package/dist/solana/swaps/programTypes.js +945 -945
  70. package/dist/solana/wallet/SolanaKeypairWallet.d.ts +9 -9
  71. package/dist/solana/wallet/SolanaKeypairWallet.js +33 -33
  72. package/dist/solana/wallet/SolanaSigner.d.ts +11 -11
  73. package/dist/solana/wallet/SolanaSigner.js +17 -17
  74. package/dist/utils/Utils.d.ts +53 -53
  75. package/dist/utils/Utils.js +170 -170
  76. package/package.json +41 -41
  77. package/src/index.ts +36 -36
  78. package/src/solana/SolanaChainType.ts +27 -27
  79. package/src/solana/SolanaChains.ts +23 -23
  80. package/src/solana/SolanaInitializer.ts +102 -102
  81. package/src/solana/btcrelay/SolanaBtcRelay.ts +589 -589
  82. package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +57 -57
  83. package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +102 -102
  84. package/src/solana/btcrelay/program/programIdl.json +670 -670
  85. package/src/solana/chain/SolanaAction.ts +108 -108
  86. package/src/solana/chain/SolanaChainInterface.ts +192 -192
  87. package/src/solana/chain/SolanaModule.ts +20 -20
  88. package/src/solana/chain/modules/SolanaAddresses.ts +20 -20
  89. package/src/solana/chain/modules/SolanaBlocks.ts +78 -78
  90. package/src/solana/chain/modules/SolanaEvents.ts +256 -256
  91. package/src/solana/chain/modules/SolanaFees.ts +450 -450
  92. package/src/solana/chain/modules/SolanaSignatures.ts +39 -39
  93. package/src/solana/chain/modules/SolanaSlots.ts +82 -82
  94. package/src/solana/chain/modules/SolanaTokens.ts +307 -307
  95. package/src/solana/chain/modules/SolanaTransactions.ts +365 -365
  96. package/src/solana/events/SolanaChainEvents.ts +299 -299
  97. package/src/solana/events/SolanaChainEventsBrowser.ts +209 -209
  98. package/src/solana/program/SolanaProgramBase.ts +79 -79
  99. package/src/solana/program/SolanaProgramModule.ts +15 -15
  100. package/src/solana/program/modules/SolanaProgramEvents.ts +155 -155
  101. package/src/solana/swaps/SolanaSwapData.ts +430 -430
  102. package/src/solana/swaps/SolanaSwapModule.ts +16 -16
  103. package/src/solana/swaps/SolanaSwapProgram.ts +854 -849
  104. package/src/solana/swaps/SwapTypeEnum.ts +29 -29
  105. package/src/solana/swaps/modules/SolanaDataAccount.ts +307 -307
  106. package/src/solana/swaps/modules/SolanaLpVault.ts +215 -215
  107. package/src/solana/swaps/modules/SwapClaim.ts +389 -389
  108. package/src/solana/swaps/modules/SwapInit.ts +663 -663
  109. package/src/solana/swaps/modules/SwapRefund.ts +323 -323
  110. package/src/solana/swaps/programIdl.json +944 -944
  111. package/src/solana/swaps/programTypes.ts +1885 -1885
  112. package/src/solana/wallet/SolanaKeypairWallet.ts +36 -36
  113. package/src/solana/wallet/SolanaSigner.ts +24 -24
  114. package/src/utils/Utils.ts +180 -180
@@ -1,664 +1,664 @@
1
- import {ParsedAccountsModeBlockResponse, PublicKey, SystemProgram, Transaction} from "@solana/web3.js";
2
- import {
3
- SignatureVerificationError,
4
- SwapCommitStateType,
5
- SwapDataVerificationError
6
- } from "@atomiqlabs/base";
7
- import {SolanaSwapData} from "../SolanaSwapData";
8
- import {SolanaAction} from "../../chain/SolanaAction";
9
- import {
10
- Account,
11
- createAssociatedTokenAccountIdempotentInstruction,
12
- getAssociatedTokenAddressSync,
13
- TOKEN_PROGRAM_ID
14
- } from "@solana/spl-token";
15
- import {SolanaSwapModule} from "../SolanaSwapModule";
16
- import {SolanaTx} from "../../chain/modules/SolanaTransactions";
17
- import {toBN, tryWithRetries} from "../../../utils/Utils";
18
- import {Buffer} from "buffer";
19
- import {SolanaSigner} from "../../wallet/SolanaSigner";
20
- import {SolanaTokens} from "../../chain/modules/SolanaTokens";
21
-
22
- export type SolanaPreFetchVerification = {
23
- latestSlot?: {
24
- slot: number,
25
- timestamp: number
26
- },
27
- transactionSlot?: {
28
- slot: number,
29
- blockhash: string
30
- }
31
- };
32
-
33
- export type SolanaPreFetchData = {
34
- block: ParsedAccountsModeBlockResponse,
35
- slot: number,
36
- timestamp: number
37
- }
38
-
39
- export class SwapInit extends SolanaSwapModule {
40
-
41
- public readonly SIGNATURE_SLOT_BUFFER = 20;
42
- public readonly SIGNATURE_PREFETCH_DATA_VALIDITY = 5000;
43
-
44
- private static readonly CUCosts = {
45
- INIT: 90000,
46
- INIT_PAY_IN: 50000,
47
- };
48
-
49
- /**
50
- * bare Init action based on the data passed in swapData
51
- *
52
- * @param swapData
53
- * @param timeout
54
- * @private
55
- */
56
- private async Init(swapData: SolanaSwapData, timeout: bigint): Promise<SolanaAction> {
57
- const claimerAta = getAssociatedTokenAddressSync(swapData.token, swapData.claimer);
58
- const paymentHash = Buffer.from(swapData.paymentHash, "hex");
59
- const accounts = {
60
- claimer: swapData.claimer,
61
- offerer: swapData.offerer,
62
- escrowState: this.program.SwapEscrowState(paymentHash),
63
- mint: swapData.token,
64
- systemProgram: SystemProgram.programId,
65
- claimerAta: swapData.payOut ? claimerAta : null,
66
- claimerUserData: !swapData.payOut ? this.program.SwapUserVault(swapData.claimer, swapData.token) : null
67
- };
68
-
69
- if(swapData.payIn) {
70
- const ata = getAssociatedTokenAddressSync(swapData.token, swapData.offerer);
71
-
72
- return new SolanaAction(swapData.offerer, this.root,
73
- await this.swapProgram.methods
74
- .offererInitializePayIn(
75
- swapData.toSwapDataStruct(),
76
- [...Buffer.alloc(32, 0)],
77
- toBN(timeout),
78
- )
79
- .accounts({
80
- ...accounts,
81
- offererAta: ata,
82
- vault: this.program.SwapVault(swapData.token),
83
- vaultAuthority: this.program.SwapVaultAuthority,
84
- tokenProgram: TOKEN_PROGRAM_ID,
85
- })
86
- .instruction(),
87
- SwapInit.CUCosts.INIT_PAY_IN
88
- );
89
- } else {
90
- return new SolanaAction(swapData.claimer, this.root,
91
- await this.swapProgram.methods
92
- .offererInitialize(
93
- swapData.toSwapDataStruct(),
94
- swapData.securityDeposit,
95
- swapData.claimerBounty,
96
- [...(swapData.txoHash!=null ? Buffer.from(swapData.txoHash, "hex") : Buffer.alloc(32, 0))],
97
- toBN(timeout)
98
- )
99
- .accounts({
100
- ...accounts,
101
- offererUserData: this.program.SwapUserVault(swapData.offerer, swapData.token),
102
- })
103
- .instruction(),
104
- SwapInit.CUCosts.INIT
105
- );
106
- }
107
- }
108
-
109
- /**
110
- * InitPayIn action which includes SOL to WSOL wrapping if indicated by the fee rate
111
- *
112
- * @param swapData
113
- * @param timeout
114
- * @param feeRate
115
- * @constructor
116
- * @private
117
- */
118
- private async InitPayIn(swapData: SolanaSwapData, timeout: bigint, feeRate: string): Promise<SolanaAction> {
119
- if(!swapData.isPayIn()) throw new Error("Must be payIn==true");
120
- const action = new SolanaAction(swapData.offerer, this.root);
121
- if(this.shouldWrapOnInit(swapData, feeRate)) action.addAction(this.Wrap(swapData, feeRate));
122
- action.addAction(await this.Init(swapData, timeout));
123
- return action;
124
- }
125
-
126
- /**
127
- * InitNotPayIn action with additional createAssociatedTokenAccountIdempotentInstruction instruction, such that
128
- * a recipient ATA is created if it doesn't exist
129
- *
130
- * @param swapData
131
- * @param timeout
132
- * @constructor
133
- * @private
134
- */
135
- private async InitNotPayIn(swapData: SolanaSwapData, timeout: bigint): Promise<SolanaAction> {
136
- if(swapData.isPayIn()) throw new Error("Must be payIn==false");
137
- const action = new SolanaAction(swapData.claimer, this.root);
138
- action.addIx(
139
- createAssociatedTokenAccountIdempotentInstruction(
140
- swapData.claimer,
141
- swapData.claimerAta,
142
- swapData.claimer,
143
- swapData.token
144
- )
145
- );
146
- action.addAction(await this.Init(swapData, timeout));
147
- return action;
148
- }
149
-
150
- private Wrap(
151
- swapData: SolanaSwapData,
152
- feeRate: string
153
- ): SolanaAction {
154
- const data = this.extractAtaDataFromFeeRate(feeRate);
155
- if(data==null) throw new Error("Tried to add wrap instruction, but feeRate malformed: "+feeRate);
156
- return this.root.Tokens.Wrap(swapData.offerer, swapData.getAmount() - data.balance, data.initAta);
157
- }
158
-
159
- /**
160
- * Extracts data about SOL to WSOL wrapping from the fee rate, fee rate is used to convey this information from
161
- * the user to the intermediary, such that the intermediary creates valid signature for transaction including
162
- * the SOL to WSOL wrapping instructions
163
- *
164
- * @param feeRate
165
- * @private
166
- */
167
- private extractAtaDataFromFeeRate(feeRate: string): {balance: bigint, initAta: boolean} | null {
168
- const hashArr = feeRate==null ? [] : feeRate.split("#");
169
- if(hashArr.length<=1) return null;
170
-
171
- const arr = hashArr[1].split(";");
172
- if(arr.length<=1) return null;
173
-
174
- return {
175
- balance: BigInt(arr[1]),
176
- initAta: arr[0]==="1"
177
- }
178
- }
179
-
180
- /**
181
- * Checks whether a wrap instruction (SOL -> WSOL) should be a part of the signed init message
182
- *
183
- * @param swapData
184
- * @param feeRate
185
- * @private
186
- * @returns {boolean} returns true if wrap instruction should be added
187
- */
188
- private shouldWrapOnInit(swapData: SolanaSwapData, feeRate: string): boolean {
189
- const data = this.extractAtaDataFromFeeRate(feeRate);
190
- if(data==null) return false;
191
- return data.balance < swapData.getAmount();
192
- }
193
-
194
- /**
195
- * Returns the transaction to be signed as an initialization signature from the intermediary, also adds
196
- * SOL to WSOL wrapping if indicated by the fee rate
197
- *
198
- * @param swapData
199
- * @param timeout
200
- * @param feeRate
201
- * @private
202
- */
203
- private async getTxToSign(swapData: SolanaSwapData, timeout: string, feeRate?: string): Promise<Transaction> {
204
- const action = swapData.isPayIn() ?
205
- await this.InitPayIn(swapData, BigInt(timeout), feeRate) :
206
- await this.InitNotPayIn(swapData, BigInt(timeout));
207
-
208
- const tx = (await action.tx(feeRate)).tx;
209
- return tx;
210
- }
211
-
212
- /**
213
- * Returns auth prefix to be used with a specific swap, payIn=true & payIn=false use different prefixes (these
214
- * actually have no meaning for the smart contract/solana program in the Solana case)
215
- *
216
- * @param swapData
217
- * @private
218
- */
219
- private getAuthPrefix(swapData: SolanaSwapData): string {
220
- return swapData.isPayIn() ? "claim_initialize" : "initialize";
221
- }
222
-
223
- /**
224
- * Returns "processed" slot required for signature validation, uses preFetchedData if provided & valid
225
- *
226
- * @param preFetchedData
227
- * @private
228
- */
229
- private getSlotForSignature(preFetchedData?: SolanaPreFetchVerification): Promise<number> {
230
- if(
231
- preFetchedData!=null &&
232
- preFetchedData.latestSlot!=null &&
233
- preFetchedData.latestSlot.timestamp>Date.now()-this.root.Slots.SLOT_CACHE_TIME
234
- ) {
235
- const estimatedSlotsPassed = Math.floor((Date.now()-preFetchedData.latestSlot.timestamp)/this.root.SLOT_TIME);
236
- const estimatedCurrentSlot = preFetchedData.latestSlot.slot+estimatedSlotsPassed;
237
- this.logger.debug("getSlotForSignature(): slot: "+preFetchedData.latestSlot.slot+
238
- " estimated passed slots: "+estimatedSlotsPassed+" estimated current slot: "+estimatedCurrentSlot);
239
- return Promise.resolve(estimatedCurrentSlot);
240
- }
241
- return this.root.Slots.getSlot("processed");
242
- }
243
-
244
- /**
245
- * Returns blockhash required for signature validation, uses preFetchedData if provided & valid
246
- *
247
- * @param txSlot
248
- * @param preFetchedData
249
- * @private
250
- */
251
- private getBlockhashForSignature(txSlot: number, preFetchedData?: SolanaPreFetchVerification): Promise<string> {
252
- if(
253
- preFetchedData!=null &&
254
- preFetchedData.transactionSlot!=null &&
255
- preFetchedData.transactionSlot.slot===txSlot
256
- ) {
257
- return Promise.resolve(preFetchedData.transactionSlot.blockhash);
258
- }
259
- return this.root.Blocks.getParsedBlock(txSlot).then(val => val.blockhash);
260
- }
261
-
262
- /**
263
- * Pre-fetches slot & block based on priorly received SolanaPreFetchData, such that it can later be used
264
- * by signature verification
265
- *
266
- * @param data
267
- */
268
- public async preFetchForInitSignatureVerification(data: SolanaPreFetchData): Promise<SolanaPreFetchVerification> {
269
- const [latestSlot, txBlock] = await Promise.all([
270
- this.root.Slots.getSlotAndTimestamp("processed"),
271
- this.root.Blocks.getParsedBlock(data.slot)
272
- ]);
273
- return {
274
- latestSlot,
275
- transactionSlot: {
276
- slot: data.slot,
277
- blockhash: txBlock.blockhash
278
- }
279
- }
280
- }
281
-
282
- /**
283
- * Pre-fetches block data required for signing the init message by the LP, this can happen in parallel before
284
- * signing takes place making the quoting quicker
285
- */
286
- public async preFetchBlockDataForSignatures(): Promise<SolanaPreFetchData> {
287
- const latestParsedBlock = await this.root.Blocks.findLatestParsedBlock("finalized");
288
- return {
289
- block: latestParsedBlock.block,
290
- slot: latestParsedBlock.slot,
291
- timestamp: Date.now()
292
- };
293
- }
294
-
295
- /**
296
- * Signs swap initialization authorization, using data from preFetchedBlockData if provided & still valid (subject
297
- * to SIGNATURE_PREFETCH_DATA_VALIDITY)
298
- *
299
- * @param signer
300
- * @param swapData
301
- * @param authorizationTimeout
302
- * @param feeRate
303
- * @param preFetchedBlockData
304
- * @public
305
- */
306
- public async signSwapInitialization(
307
- signer: SolanaSigner,
308
- swapData: SolanaSwapData,
309
- authorizationTimeout: number,
310
- preFetchedBlockData?: SolanaPreFetchData,
311
- feeRate?: string
312
- ): Promise<{prefix: string, timeout: string, signature: string}> {
313
- if(signer.keypair==null) throw new Error("Unsupported");
314
- if(!signer.getPublicKey().equals(swapData.isPayIn() ? swapData.claimer : swapData.offerer)) throw new Error("Invalid signer, wrong public key!");
315
-
316
- if(preFetchedBlockData!=null && Date.now()-preFetchedBlockData.timestamp>this.SIGNATURE_PREFETCH_DATA_VALIDITY) preFetchedBlockData = null;
317
-
318
- const {
319
- block: latestBlock,
320
- slot: latestSlot
321
- } = preFetchedBlockData || await this.root.Blocks.findLatestParsedBlock("finalized");
322
-
323
- const authTimeout = Math.floor(Date.now()/1000)+authorizationTimeout;
324
- const txToSign = await this.getTxToSign(swapData, authTimeout.toString(10), feeRate);
325
- txToSign.feePayer = swapData.isPayIn() ? swapData.offerer : swapData.claimer;
326
- txToSign.recentBlockhash = latestBlock.blockhash;
327
- txToSign.sign(signer.keypair);
328
- this.logger.debug("signSwapInitialization(): Signed tx: ",txToSign);
329
-
330
- const sig = txToSign.signatures.find(e => e.publicKey.equals(signer.getPublicKey()));
331
-
332
- return {
333
- prefix: this.getAuthPrefix(swapData),
334
- timeout: authTimeout.toString(10),
335
- signature: latestSlot+";"+sig.signature.toString("hex")
336
- };
337
- }
338
-
339
- /**
340
- * Checks whether the provided signature data is valid, using preFetchedData if provided and still valid
341
- *
342
- * @param sender
343
- * @param swapData
344
- * @param timeout
345
- * @param prefix
346
- * @param signature
347
- * @param feeRate
348
- * @param preFetchedData
349
- * @public
350
- */
351
- public async isSignatureValid(
352
- sender: string,
353
- swapData: SolanaSwapData,
354
- timeout: string,
355
- prefix: string,
356
- signature: string,
357
- feeRate?: string,
358
- preFetchedData?: SolanaPreFetchVerification
359
- ): Promise<Buffer> {
360
- if(swapData.isPayIn()) {
361
- if(!swapData.isOfferer(sender)) throw new SignatureVerificationError("Sender needs to be offerer in payIn=true swaps");
362
- } else {
363
- if(!swapData.isClaimer(sender)) throw new SignatureVerificationError("Sender needs to be claimer in payIn=false swaps");
364
- }
365
-
366
- const signer = swapData.isPayIn() ? swapData.claimer : swapData.offerer;
367
-
368
- if(!swapData.isPayIn() && await this.program.isExpired(sender.toString(), swapData)) {
369
- throw new SignatureVerificationError("Swap will expire too soon!");
370
- }
371
-
372
- if(prefix!==this.getAuthPrefix(swapData)) throw new SignatureVerificationError("Invalid prefix");
373
-
374
- const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
375
- const isExpired = (BigInt(timeout) - currentTimestamp) < BigInt(this.program.authGracePeriod);
376
- if (isExpired) throw new SignatureVerificationError("Authorization expired!");
377
-
378
- const [transactionSlot, signatureString] = signature.split(";");
379
- const txSlot = parseInt(transactionSlot);
380
-
381
- const [latestSlot, blockhash] = await Promise.all([
382
- this.getSlotForSignature(preFetchedData),
383
- this.getBlockhashForSignature(txSlot, preFetchedData)
384
- ]);
385
-
386
- const lastValidTransactionSlot = txSlot+this.root.TX_SLOT_VALIDITY;
387
- const slotsLeft = lastValidTransactionSlot-latestSlot-this.SIGNATURE_SLOT_BUFFER;
388
- if(slotsLeft<0) throw new SignatureVerificationError("Authorization expired!");
389
-
390
- const txToSign = await this.getTxToSign(swapData, timeout, feeRate);
391
- txToSign.feePayer = new PublicKey(sender);
392
- txToSign.recentBlockhash = blockhash;
393
- txToSign.addSignature(signer, Buffer.from(signatureString, "hex"));
394
- this.logger.debug("isSignatureValid(): Signed tx: ",txToSign);
395
-
396
- const valid = txToSign.verifySignatures(false);
397
-
398
- if(!valid) throw new SignatureVerificationError("Invalid signature!");
399
-
400
- return Buffer.from(blockhash);
401
- }
402
-
403
- /**
404
- * Gets expiry of the provided signature data, this is a minimum of slot expiry & swap signature expiry
405
- *
406
- * @param timeout
407
- * @param signature
408
- * @param preFetchedData
409
- * @public
410
- */
411
- public async getSignatureExpiry(
412
- timeout: string,
413
- signature: string,
414
- preFetchedData?: SolanaPreFetchVerification
415
- ): Promise<number> {
416
- const [transactionSlotStr, signatureString] = signature.split(";");
417
- const txSlot = parseInt(transactionSlotStr);
418
-
419
- const latestSlot = await this.getSlotForSignature(preFetchedData);
420
- const lastValidTransactionSlot = txSlot+this.root.TX_SLOT_VALIDITY;
421
- const slotsLeft = lastValidTransactionSlot-latestSlot-this.SIGNATURE_SLOT_BUFFER;
422
-
423
- const now = Date.now();
424
-
425
- const slotExpiryTime = now + (slotsLeft*this.root.SLOT_TIME);
426
- const timeoutExpiryTime = (parseInt(timeout)-this.program.authGracePeriod)*1000;
427
- const expiry = Math.min(slotExpiryTime, timeoutExpiryTime);
428
-
429
- if(expiry<now) return 0;
430
-
431
- return expiry;
432
- }
433
-
434
- /**
435
- * Checks whether signature is expired for good (uses "finalized" slot)
436
- *
437
- * @param signature
438
- * @param timeout
439
- * @public
440
- */
441
- public async isSignatureExpired(
442
- signature: string,
443
- timeout: string
444
- ): Promise<boolean> {
445
- const [transactionSlotStr, signatureString] = signature.split(";");
446
- const txSlot = parseInt(transactionSlotStr);
447
-
448
- const lastValidTransactionSlot = txSlot+this.root.TX_SLOT_VALIDITY;
449
- const latestSlot = await this.root.Slots.getSlot("finalized");
450
- const slotsLeft = lastValidTransactionSlot-latestSlot+this.SIGNATURE_SLOT_BUFFER;
451
-
452
- if(slotsLeft<0) return true;
453
- if((parseInt(timeout)+this.program.authGracePeriod)*1000 < Date.now()) return true;
454
- return false;
455
- }
456
-
457
- /**
458
- * Creates init transaction (InitPayIn) with a valid signature from an LP, also adds a SOL to WSOL wrapping ix to
459
- * the init transaction (if indicated by the fee rate) or adds the wrapping in a separate transaction (if no
460
- * indication in the fee rate)
461
- *
462
- * @param swapData swap to initialize
463
- * @param timeout init signature timeout
464
- * @param prefix init signature prefix
465
- * @param signature init signature
466
- * @param skipChecks whether to skip signature validity checks
467
- * @param feeRate fee rate to use for the transaction
468
- */
469
- public async txsInitPayIn(
470
- swapData: SolanaSwapData,
471
- timeout: string,
472
- prefix: string,
473
- signature: string,
474
- skipChecks?: boolean,
475
- feeRate?: string
476
- ): Promise<SolanaTx[]> {
477
- if(!skipChecks) {
478
- const [_, payStatus] = await Promise.all([
479
- tryWithRetries(
480
- () => this.isSignatureValid(swapData.getOfferer(), swapData, timeout, prefix, signature, feeRate),
481
- this.retryPolicy, (e) => e instanceof SignatureVerificationError
482
- ),
483
- tryWithRetries(() => this.program.getClaimHashStatus(swapData.getClaimHash()), this.retryPolicy)
484
- ]);
485
- if(payStatus!==SwapCommitStateType.NOT_COMMITED) throw new SwapDataVerificationError("Invoice already being paid for or paid");
486
- }
487
-
488
- const [slotNumber, signatureStr] = signature.split(";");
489
- const block = await tryWithRetries(
490
- () => this.root.Blocks.getParsedBlock(parseInt(slotNumber)),
491
- this.retryPolicy
492
- );
493
-
494
- const txs: SolanaTx[] = [];
495
-
496
- let isWrapping: boolean = false;
497
- const isWrappedInSignedTx = feeRate!=null && feeRate.split("#").length>1;
498
- if(!isWrappedInSignedTx && swapData.token.equals(SolanaTokens.WSOL_ADDRESS)) {
499
- const ataAcc = await tryWithRetries<Account>(
500
- () => this.root.Tokens.getATAOrNull(swapData.offererAta),
501
- this.retryPolicy
502
- );
503
- const balance: bigint = ataAcc==null ? 0n : ataAcc.amount;
504
-
505
- if(balance < swapData.getAmount()) {
506
- //Need to wrap more SOL to WSOL
507
- await this.root.Tokens.Wrap(swapData.offerer, swapData.getAmount() - balance, ataAcc==null)
508
- .addToTxs(txs, feeRate, block);
509
- isWrapping = true;
510
- }
511
- }
512
-
513
- const initTx = await (await this.InitPayIn(swapData, BigInt(timeout), feeRate)).tx(feeRate, block);
514
- initTx.tx.addSignature(swapData.claimer, Buffer.from(signatureStr, "hex"));
515
- txs.push(initTx);
516
-
517
- this.logger.debug("txsInitPayIn(): create swap init TX, swap: "+swapData.getClaimHash()+
518
- " wrapping client-side: "+isWrapping+" feerate: "+feeRate);
519
-
520
- return txs;
521
- }
522
-
523
- /**
524
- * Creates init transactions (InitNotPayIn) with a valid signature from an intermediary
525
- *
526
- * @param swapData swap to initialize
527
- * @param timeout init signature timeout
528
- * @param prefix init signature prefix
529
- * @param signature init signature
530
- * @param skipChecks whether to skip signature validity checks
531
- * @param feeRate fee rate to use for the transaction
532
- */
533
- public async txsInit(swapData: SolanaSwapData, timeout: string, prefix: string, signature: string, skipChecks?: boolean, feeRate?: string): Promise<SolanaTx[]> {
534
- if(!skipChecks) {
535
- await tryWithRetries(
536
- () => this.isSignatureValid(swapData.getClaimer(), swapData, timeout, prefix, signature, feeRate),
537
- this.retryPolicy,
538
- (e) => e instanceof SignatureVerificationError
539
- );
540
- }
541
-
542
- const [slotNumber, signatureStr] = signature.split(";");
543
- const block = await tryWithRetries(
544
- () => this.root.Blocks.getParsedBlock(parseInt(slotNumber)),
545
- this.retryPolicy
546
- );
547
-
548
- const initTx = await (await this.InitNotPayIn(swapData, BigInt(timeout))).tx(feeRate, block);
549
- initTx.tx.addSignature(swapData.offerer, Buffer.from(signatureStr, "hex"));
550
-
551
- this.logger.debug("txsInit(): create swap init TX, swap: "+swapData.getClaimHash()+" feerate: "+feeRate);
552
-
553
- return [initTx];
554
- }
555
-
556
- /**
557
- * Returns the fee rate to be used for a specific init transaction, also adding indication whether the WSOL ATA
558
- * should be initialized in the init transaction and/or current balance in the WSOL ATA
559
- *
560
- * @param offerer
561
- * @param claimer
562
- * @param token
563
- * @param paymentHash
564
- */
565
- public async getInitPayInFeeRate(offerer?: PublicKey, claimer?: PublicKey, token?: PublicKey, paymentHash?: string): Promise<string> {
566
- const accounts: PublicKey[] = [];
567
-
568
- if (offerer != null) accounts.push(offerer);
569
- if (token != null) {
570
- accounts.push(this.program.SwapVault(token));
571
- if (offerer != null) accounts.push(getAssociatedTokenAddressSync(token, offerer));
572
- if (claimer != null) accounts.push(this.program.SwapUserVault(claimer, token));
573
- }
574
- if (paymentHash != null) accounts.push(this.program.SwapEscrowState(Buffer.from(paymentHash, "hex")));
575
-
576
- const shouldCheckWSOLAta = token != null && offerer != null && token.equals(SolanaTokens.WSOL_ADDRESS);
577
- let [feeRate, _account] = await Promise.all([
578
- this.root.Fees.getFeeRate(accounts),
579
- shouldCheckWSOLAta ?
580
- this.root.Tokens.getATAOrNull(getAssociatedTokenAddressSync(token, offerer)) :
581
- Promise.resolve(null)
582
- ]);
583
-
584
- if(shouldCheckWSOLAta) {
585
- const account: Account = _account;
586
- const balance: bigint = account == null ? 0n : account.amount;
587
- //Add an indication about whether the ATA is initialized & balance it contains
588
- feeRate += "#" + (account != null ? "0" : "1") + ";" + balance.toString(10);
589
- }
590
-
591
- this.logger.debug("getInitPayInFeeRate(): feerate computed: "+feeRate);
592
- return feeRate;
593
- }
594
-
595
- /**
596
- * Returns the fee rate to be used for a specific init transaction
597
- *
598
- * @param offerer
599
- * @param claimer
600
- * @param token
601
- * @param paymentHash
602
- */
603
- public getInitFeeRate(offerer?: PublicKey, claimer?: PublicKey, token?: PublicKey, paymentHash?: string): Promise<string> {
604
- const accounts: PublicKey[] = [];
605
-
606
- if(offerer!=null && token!=null) accounts.push(this.program.SwapUserVault(offerer, token));
607
- if(claimer!=null) accounts.push(claimer)
608
- if(paymentHash!=null) accounts.push(this.program.SwapEscrowState(Buffer.from(paymentHash, "hex")));
609
-
610
- return this.root.Fees.getFeeRate(accounts);
611
- }
612
-
613
- /**
614
- * Get the estimated solana fee of the init transaction, this includes the required deposit for creating swap PDA
615
- * and also deposit for ATAs
616
- */
617
- async getInitFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
618
- if(swapData==null) return BigInt(this.program.ESCROW_STATE_RENT_EXEMPT) + await this.getRawInitFee(swapData, feeRate);
619
-
620
- feeRate = feeRate ||
621
- (swapData.payIn
622
- ? await this.getInitPayInFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash)
623
- : await this.getInitFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash));
624
-
625
- const [rawFee, initAta] = await Promise.all([
626
- this.getRawInitFee(swapData, feeRate),
627
- swapData!=null && swapData.payOut ?
628
- this.root.Tokens.getATAOrNull(getAssociatedTokenAddressSync(swapData.token, swapData.claimer)).then(acc => acc==null) :
629
- Promise.resolve<null>(null)
630
- ]);
631
-
632
- let resultingFee = BigInt(this.program.ESCROW_STATE_RENT_EXEMPT) + rawFee;
633
- if(initAta) resultingFee += BigInt(SolanaTokens.SPL_ATA_RENT_EXEMPT);
634
-
635
- if(swapData.payIn && this.shouldWrapOnInit(swapData, feeRate) && this.extractAtaDataFromFeeRate(feeRate).initAta) {
636
- resultingFee += BigInt(SolanaTokens.SPL_ATA_RENT_EXEMPT);
637
- }
638
-
639
- return resultingFee;
640
- }
641
-
642
- /**
643
- * Get the estimated solana fee of the init transaction, without the required deposit for creating swap PDA
644
- */
645
- async getRawInitFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
646
- if(swapData==null) return 10000n;
647
-
648
- feeRate = feeRate ||
649
- (swapData.payIn
650
- ? await this.getInitPayInFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash)
651
- : await this.getInitFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash));
652
-
653
- let computeBudget = swapData.payIn ? SwapInit.CUCosts.INIT_PAY_IN : SwapInit.CUCosts.INIT;
654
- if(swapData.payIn && this.shouldWrapOnInit(swapData, feeRate)) {
655
- computeBudget += SolanaTokens.CUCosts.WRAP_SOL;
656
- const data = this.extractAtaDataFromFeeRate(feeRate);
657
- if(data.initAta) computeBudget += SolanaTokens.CUCosts.ATA_INIT;
658
- }
659
- const baseFee = swapData.payIn ? 10000n : 10000n + 5000n;
660
-
661
- return baseFee + this.root.Fees.getPriorityFee(computeBudget, feeRate);
662
- }
663
-
1
+ import {ParsedAccountsModeBlockResponse, PublicKey, SystemProgram, Transaction} from "@solana/web3.js";
2
+ import {
3
+ SignatureVerificationError,
4
+ SwapCommitStateType,
5
+ SwapDataVerificationError
6
+ } from "@atomiqlabs/base";
7
+ import {SolanaSwapData} from "../SolanaSwapData";
8
+ import {SolanaAction} from "../../chain/SolanaAction";
9
+ import {
10
+ Account,
11
+ createAssociatedTokenAccountIdempotentInstruction,
12
+ getAssociatedTokenAddressSync,
13
+ TOKEN_PROGRAM_ID
14
+ } from "@solana/spl-token";
15
+ import {SolanaSwapModule} from "../SolanaSwapModule";
16
+ import {SolanaTx} from "../../chain/modules/SolanaTransactions";
17
+ import {toBN, tryWithRetries} from "../../../utils/Utils";
18
+ import {Buffer} from "buffer";
19
+ import {SolanaSigner} from "../../wallet/SolanaSigner";
20
+ import {SolanaTokens} from "../../chain/modules/SolanaTokens";
21
+
22
+ export type SolanaPreFetchVerification = {
23
+ latestSlot?: {
24
+ slot: number,
25
+ timestamp: number
26
+ },
27
+ transactionSlot?: {
28
+ slot: number,
29
+ blockhash: string
30
+ }
31
+ };
32
+
33
+ export type SolanaPreFetchData = {
34
+ block: ParsedAccountsModeBlockResponse,
35
+ slot: number,
36
+ timestamp: number
37
+ }
38
+
39
+ export class SwapInit extends SolanaSwapModule {
40
+
41
+ public readonly SIGNATURE_SLOT_BUFFER = 20;
42
+ public readonly SIGNATURE_PREFETCH_DATA_VALIDITY = 5000;
43
+
44
+ private static readonly CUCosts = {
45
+ INIT: 90000,
46
+ INIT_PAY_IN: 50000,
47
+ };
48
+
49
+ /**
50
+ * bare Init action based on the data passed in swapData
51
+ *
52
+ * @param swapData
53
+ * @param timeout
54
+ * @private
55
+ */
56
+ private async Init(swapData: SolanaSwapData, timeout: bigint): Promise<SolanaAction> {
57
+ const claimerAta = getAssociatedTokenAddressSync(swapData.token, swapData.claimer);
58
+ const paymentHash = Buffer.from(swapData.paymentHash, "hex");
59
+ const accounts = {
60
+ claimer: swapData.claimer,
61
+ offerer: swapData.offerer,
62
+ escrowState: this.program.SwapEscrowState(paymentHash),
63
+ mint: swapData.token,
64
+ systemProgram: SystemProgram.programId,
65
+ claimerAta: swapData.payOut ? claimerAta : null,
66
+ claimerUserData: !swapData.payOut ? this.program.SwapUserVault(swapData.claimer, swapData.token) : null
67
+ };
68
+
69
+ if(swapData.payIn) {
70
+ const ata = getAssociatedTokenAddressSync(swapData.token, swapData.offerer);
71
+
72
+ return new SolanaAction(swapData.offerer, this.root,
73
+ await this.swapProgram.methods
74
+ .offererInitializePayIn(
75
+ swapData.toSwapDataStruct(),
76
+ [...Buffer.alloc(32, 0)],
77
+ toBN(timeout),
78
+ )
79
+ .accounts({
80
+ ...accounts,
81
+ offererAta: ata,
82
+ vault: this.program.SwapVault(swapData.token),
83
+ vaultAuthority: this.program.SwapVaultAuthority,
84
+ tokenProgram: TOKEN_PROGRAM_ID,
85
+ })
86
+ .instruction(),
87
+ SwapInit.CUCosts.INIT_PAY_IN
88
+ );
89
+ } else {
90
+ return new SolanaAction(swapData.claimer, this.root,
91
+ await this.swapProgram.methods
92
+ .offererInitialize(
93
+ swapData.toSwapDataStruct(),
94
+ swapData.securityDeposit,
95
+ swapData.claimerBounty,
96
+ [...(swapData.txoHash!=null ? Buffer.from(swapData.txoHash, "hex") : Buffer.alloc(32, 0))],
97
+ toBN(timeout)
98
+ )
99
+ .accounts({
100
+ ...accounts,
101
+ offererUserData: this.program.SwapUserVault(swapData.offerer, swapData.token),
102
+ })
103
+ .instruction(),
104
+ SwapInit.CUCosts.INIT
105
+ );
106
+ }
107
+ }
108
+
109
+ /**
110
+ * InitPayIn action which includes SOL to WSOL wrapping if indicated by the fee rate
111
+ *
112
+ * @param swapData
113
+ * @param timeout
114
+ * @param feeRate
115
+ * @constructor
116
+ * @private
117
+ */
118
+ private async InitPayIn(swapData: SolanaSwapData, timeout: bigint, feeRate: string): Promise<SolanaAction> {
119
+ if(!swapData.isPayIn()) throw new Error("Must be payIn==true");
120
+ const action = new SolanaAction(swapData.offerer, this.root);
121
+ if(this.shouldWrapOnInit(swapData, feeRate)) action.addAction(this.Wrap(swapData, feeRate));
122
+ action.addAction(await this.Init(swapData, timeout));
123
+ return action;
124
+ }
125
+
126
+ /**
127
+ * InitNotPayIn action with additional createAssociatedTokenAccountIdempotentInstruction instruction, such that
128
+ * a recipient ATA is created if it doesn't exist
129
+ *
130
+ * @param swapData
131
+ * @param timeout
132
+ * @constructor
133
+ * @private
134
+ */
135
+ private async InitNotPayIn(swapData: SolanaSwapData, timeout: bigint): Promise<SolanaAction> {
136
+ if(swapData.isPayIn()) throw new Error("Must be payIn==false");
137
+ const action = new SolanaAction(swapData.claimer, this.root);
138
+ action.addIx(
139
+ createAssociatedTokenAccountIdempotentInstruction(
140
+ swapData.claimer,
141
+ swapData.claimerAta,
142
+ swapData.claimer,
143
+ swapData.token
144
+ )
145
+ );
146
+ action.addAction(await this.Init(swapData, timeout));
147
+ return action;
148
+ }
149
+
150
+ private Wrap(
151
+ swapData: SolanaSwapData,
152
+ feeRate: string
153
+ ): SolanaAction {
154
+ const data = this.extractAtaDataFromFeeRate(feeRate);
155
+ if(data==null) throw new Error("Tried to add wrap instruction, but feeRate malformed: "+feeRate);
156
+ return this.root.Tokens.Wrap(swapData.offerer, swapData.getAmount() - data.balance, data.initAta);
157
+ }
158
+
159
+ /**
160
+ * Extracts data about SOL to WSOL wrapping from the fee rate, fee rate is used to convey this information from
161
+ * the user to the intermediary, such that the intermediary creates valid signature for transaction including
162
+ * the SOL to WSOL wrapping instructions
163
+ *
164
+ * @param feeRate
165
+ * @private
166
+ */
167
+ private extractAtaDataFromFeeRate(feeRate: string): {balance: bigint, initAta: boolean} | null {
168
+ const hashArr = feeRate==null ? [] : feeRate.split("#");
169
+ if(hashArr.length<=1) return null;
170
+
171
+ const arr = hashArr[1].split(";");
172
+ if(arr.length<=1) return null;
173
+
174
+ return {
175
+ balance: BigInt(arr[1]),
176
+ initAta: arr[0]==="1"
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Checks whether a wrap instruction (SOL -> WSOL) should be a part of the signed init message
182
+ *
183
+ * @param swapData
184
+ * @param feeRate
185
+ * @private
186
+ * @returns {boolean} returns true if wrap instruction should be added
187
+ */
188
+ private shouldWrapOnInit(swapData: SolanaSwapData, feeRate: string): boolean {
189
+ const data = this.extractAtaDataFromFeeRate(feeRate);
190
+ if(data==null) return false;
191
+ return data.balance < swapData.getAmount();
192
+ }
193
+
194
+ /**
195
+ * Returns the transaction to be signed as an initialization signature from the intermediary, also adds
196
+ * SOL to WSOL wrapping if indicated by the fee rate
197
+ *
198
+ * @param swapData
199
+ * @param timeout
200
+ * @param feeRate
201
+ * @private
202
+ */
203
+ private async getTxToSign(swapData: SolanaSwapData, timeout: string, feeRate?: string): Promise<Transaction> {
204
+ const action = swapData.isPayIn() ?
205
+ await this.InitPayIn(swapData, BigInt(timeout), feeRate) :
206
+ await this.InitNotPayIn(swapData, BigInt(timeout));
207
+
208
+ const tx = (await action.tx(feeRate)).tx;
209
+ return tx;
210
+ }
211
+
212
+ /**
213
+ * Returns auth prefix to be used with a specific swap, payIn=true & payIn=false use different prefixes (these
214
+ * actually have no meaning for the smart contract/solana program in the Solana case)
215
+ *
216
+ * @param swapData
217
+ * @private
218
+ */
219
+ private getAuthPrefix(swapData: SolanaSwapData): string {
220
+ return swapData.isPayIn() ? "claim_initialize" : "initialize";
221
+ }
222
+
223
+ /**
224
+ * Returns "processed" slot required for signature validation, uses preFetchedData if provided & valid
225
+ *
226
+ * @param preFetchedData
227
+ * @private
228
+ */
229
+ private getSlotForSignature(preFetchedData?: SolanaPreFetchVerification): Promise<number> {
230
+ if(
231
+ preFetchedData!=null &&
232
+ preFetchedData.latestSlot!=null &&
233
+ preFetchedData.latestSlot.timestamp>Date.now()-this.root.Slots.SLOT_CACHE_TIME
234
+ ) {
235
+ const estimatedSlotsPassed = Math.floor((Date.now()-preFetchedData.latestSlot.timestamp)/this.root.SLOT_TIME);
236
+ const estimatedCurrentSlot = preFetchedData.latestSlot.slot+estimatedSlotsPassed;
237
+ this.logger.debug("getSlotForSignature(): slot: "+preFetchedData.latestSlot.slot+
238
+ " estimated passed slots: "+estimatedSlotsPassed+" estimated current slot: "+estimatedCurrentSlot);
239
+ return Promise.resolve(estimatedCurrentSlot);
240
+ }
241
+ return this.root.Slots.getSlot("processed");
242
+ }
243
+
244
+ /**
245
+ * Returns blockhash required for signature validation, uses preFetchedData if provided & valid
246
+ *
247
+ * @param txSlot
248
+ * @param preFetchedData
249
+ * @private
250
+ */
251
+ private getBlockhashForSignature(txSlot: number, preFetchedData?: SolanaPreFetchVerification): Promise<string> {
252
+ if(
253
+ preFetchedData!=null &&
254
+ preFetchedData.transactionSlot!=null &&
255
+ preFetchedData.transactionSlot.slot===txSlot
256
+ ) {
257
+ return Promise.resolve(preFetchedData.transactionSlot.blockhash);
258
+ }
259
+ return this.root.Blocks.getParsedBlock(txSlot).then(val => val.blockhash);
260
+ }
261
+
262
+ /**
263
+ * Pre-fetches slot & block based on priorly received SolanaPreFetchData, such that it can later be used
264
+ * by signature verification
265
+ *
266
+ * @param data
267
+ */
268
+ public async preFetchForInitSignatureVerification(data: SolanaPreFetchData): Promise<SolanaPreFetchVerification> {
269
+ const [latestSlot, txBlock] = await Promise.all([
270
+ this.root.Slots.getSlotAndTimestamp("processed"),
271
+ this.root.Blocks.getParsedBlock(data.slot)
272
+ ]);
273
+ return {
274
+ latestSlot,
275
+ transactionSlot: {
276
+ slot: data.slot,
277
+ blockhash: txBlock.blockhash
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Pre-fetches block data required for signing the init message by the LP, this can happen in parallel before
284
+ * signing takes place making the quoting quicker
285
+ */
286
+ public async preFetchBlockDataForSignatures(): Promise<SolanaPreFetchData> {
287
+ const latestParsedBlock = await this.root.Blocks.findLatestParsedBlock("finalized");
288
+ return {
289
+ block: latestParsedBlock.block,
290
+ slot: latestParsedBlock.slot,
291
+ timestamp: Date.now()
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Signs swap initialization authorization, using data from preFetchedBlockData if provided & still valid (subject
297
+ * to SIGNATURE_PREFETCH_DATA_VALIDITY)
298
+ *
299
+ * @param signer
300
+ * @param swapData
301
+ * @param authorizationTimeout
302
+ * @param feeRate
303
+ * @param preFetchedBlockData
304
+ * @public
305
+ */
306
+ public async signSwapInitialization(
307
+ signer: SolanaSigner,
308
+ swapData: SolanaSwapData,
309
+ authorizationTimeout: number,
310
+ preFetchedBlockData?: SolanaPreFetchData,
311
+ feeRate?: string
312
+ ): Promise<{prefix: string, timeout: string, signature: string}> {
313
+ if(signer.keypair==null) throw new Error("Unsupported");
314
+ if(!signer.getPublicKey().equals(swapData.isPayIn() ? swapData.claimer : swapData.offerer)) throw new Error("Invalid signer, wrong public key!");
315
+
316
+ if(preFetchedBlockData!=null && Date.now()-preFetchedBlockData.timestamp>this.SIGNATURE_PREFETCH_DATA_VALIDITY) preFetchedBlockData = null;
317
+
318
+ const {
319
+ block: latestBlock,
320
+ slot: latestSlot
321
+ } = preFetchedBlockData || await this.root.Blocks.findLatestParsedBlock("finalized");
322
+
323
+ const authTimeout = Math.floor(Date.now()/1000)+authorizationTimeout;
324
+ const txToSign = await this.getTxToSign(swapData, authTimeout.toString(10), feeRate);
325
+ txToSign.feePayer = swapData.isPayIn() ? swapData.offerer : swapData.claimer;
326
+ txToSign.recentBlockhash = latestBlock.blockhash;
327
+ txToSign.sign(signer.keypair);
328
+ this.logger.debug("signSwapInitialization(): Signed tx: ",txToSign);
329
+
330
+ const sig = txToSign.signatures.find(e => e.publicKey.equals(signer.getPublicKey()));
331
+
332
+ return {
333
+ prefix: this.getAuthPrefix(swapData),
334
+ timeout: authTimeout.toString(10),
335
+ signature: latestSlot+";"+sig.signature.toString("hex")
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Checks whether the provided signature data is valid, using preFetchedData if provided and still valid
341
+ *
342
+ * @param sender
343
+ * @param swapData
344
+ * @param timeout
345
+ * @param prefix
346
+ * @param signature
347
+ * @param feeRate
348
+ * @param preFetchedData
349
+ * @public
350
+ */
351
+ public async isSignatureValid(
352
+ sender: string,
353
+ swapData: SolanaSwapData,
354
+ timeout: string,
355
+ prefix: string,
356
+ signature: string,
357
+ feeRate?: string,
358
+ preFetchedData?: SolanaPreFetchVerification
359
+ ): Promise<Buffer> {
360
+ if(swapData.isPayIn()) {
361
+ if(!swapData.isOfferer(sender)) throw new SignatureVerificationError("Sender needs to be offerer in payIn=true swaps");
362
+ } else {
363
+ if(!swapData.isClaimer(sender)) throw new SignatureVerificationError("Sender needs to be claimer in payIn=false swaps");
364
+ }
365
+
366
+ const signer = swapData.isPayIn() ? swapData.claimer : swapData.offerer;
367
+
368
+ if(!swapData.isPayIn() && await this.program.isExpired(sender.toString(), swapData)) {
369
+ throw new SignatureVerificationError("Swap will expire too soon!");
370
+ }
371
+
372
+ if(prefix!==this.getAuthPrefix(swapData)) throw new SignatureVerificationError("Invalid prefix");
373
+
374
+ const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
375
+ const isExpired = (BigInt(timeout) - currentTimestamp) < BigInt(this.program.authGracePeriod);
376
+ if (isExpired) throw new SignatureVerificationError("Authorization expired!");
377
+
378
+ const [transactionSlot, signatureString] = signature.split(";");
379
+ const txSlot = parseInt(transactionSlot);
380
+
381
+ const [latestSlot, blockhash] = await Promise.all([
382
+ this.getSlotForSignature(preFetchedData),
383
+ this.getBlockhashForSignature(txSlot, preFetchedData)
384
+ ]);
385
+
386
+ const lastValidTransactionSlot = txSlot+this.root.TX_SLOT_VALIDITY;
387
+ const slotsLeft = lastValidTransactionSlot-latestSlot-this.SIGNATURE_SLOT_BUFFER;
388
+ if(slotsLeft<0) throw new SignatureVerificationError("Authorization expired!");
389
+
390
+ const txToSign = await this.getTxToSign(swapData, timeout, feeRate);
391
+ txToSign.feePayer = new PublicKey(sender);
392
+ txToSign.recentBlockhash = blockhash;
393
+ txToSign.addSignature(signer, Buffer.from(signatureString, "hex"));
394
+ this.logger.debug("isSignatureValid(): Signed tx: ",txToSign);
395
+
396
+ const valid = txToSign.verifySignatures(false);
397
+
398
+ if(!valid) throw new SignatureVerificationError("Invalid signature!");
399
+
400
+ return Buffer.from(blockhash);
401
+ }
402
+
403
+ /**
404
+ * Gets expiry of the provided signature data, this is a minimum of slot expiry & swap signature expiry
405
+ *
406
+ * @param timeout
407
+ * @param signature
408
+ * @param preFetchedData
409
+ * @public
410
+ */
411
+ public async getSignatureExpiry(
412
+ timeout: string,
413
+ signature: string,
414
+ preFetchedData?: SolanaPreFetchVerification
415
+ ): Promise<number> {
416
+ const [transactionSlotStr, signatureString] = signature.split(";");
417
+ const txSlot = parseInt(transactionSlotStr);
418
+
419
+ const latestSlot = await this.getSlotForSignature(preFetchedData);
420
+ const lastValidTransactionSlot = txSlot+this.root.TX_SLOT_VALIDITY;
421
+ const slotsLeft = lastValidTransactionSlot-latestSlot-this.SIGNATURE_SLOT_BUFFER;
422
+
423
+ const now = Date.now();
424
+
425
+ const slotExpiryTime = now + (slotsLeft*this.root.SLOT_TIME);
426
+ const timeoutExpiryTime = (parseInt(timeout)-this.program.authGracePeriod)*1000;
427
+ const expiry = Math.min(slotExpiryTime, timeoutExpiryTime);
428
+
429
+ if(expiry<now) return 0;
430
+
431
+ return expiry;
432
+ }
433
+
434
+ /**
435
+ * Checks whether signature is expired for good (uses "finalized" slot)
436
+ *
437
+ * @param signature
438
+ * @param timeout
439
+ * @public
440
+ */
441
+ public async isSignatureExpired(
442
+ signature: string,
443
+ timeout: string
444
+ ): Promise<boolean> {
445
+ const [transactionSlotStr, signatureString] = signature.split(";");
446
+ const txSlot = parseInt(transactionSlotStr);
447
+
448
+ const lastValidTransactionSlot = txSlot+this.root.TX_SLOT_VALIDITY;
449
+ const latestSlot = await this.root.Slots.getSlot("finalized");
450
+ const slotsLeft = lastValidTransactionSlot-latestSlot+this.SIGNATURE_SLOT_BUFFER;
451
+
452
+ if(slotsLeft<0) return true;
453
+ if((parseInt(timeout)+this.program.authGracePeriod)*1000 < Date.now()) return true;
454
+ return false;
455
+ }
456
+
457
+ /**
458
+ * Creates init transaction (InitPayIn) with a valid signature from an LP, also adds a SOL to WSOL wrapping ix to
459
+ * the init transaction (if indicated by the fee rate) or adds the wrapping in a separate transaction (if no
460
+ * indication in the fee rate)
461
+ *
462
+ * @param swapData swap to initialize
463
+ * @param timeout init signature timeout
464
+ * @param prefix init signature prefix
465
+ * @param signature init signature
466
+ * @param skipChecks whether to skip signature validity checks
467
+ * @param feeRate fee rate to use for the transaction
468
+ */
469
+ public async txsInitPayIn(
470
+ swapData: SolanaSwapData,
471
+ timeout: string,
472
+ prefix: string,
473
+ signature: string,
474
+ skipChecks?: boolean,
475
+ feeRate?: string
476
+ ): Promise<SolanaTx[]> {
477
+ if(!skipChecks) {
478
+ const [_, payStatus] = await Promise.all([
479
+ tryWithRetries(
480
+ () => this.isSignatureValid(swapData.getOfferer(), swapData, timeout, prefix, signature, feeRate),
481
+ this.retryPolicy, (e) => e instanceof SignatureVerificationError
482
+ ),
483
+ tryWithRetries(() => this.program.getClaimHashStatus(swapData.getClaimHash()), this.retryPolicy)
484
+ ]);
485
+ if(payStatus!==SwapCommitStateType.NOT_COMMITED) throw new SwapDataVerificationError("Invoice already being paid for or paid");
486
+ }
487
+
488
+ const [slotNumber, signatureStr] = signature.split(";");
489
+ const block = await tryWithRetries(
490
+ () => this.root.Blocks.getParsedBlock(parseInt(slotNumber)),
491
+ this.retryPolicy
492
+ );
493
+
494
+ const txs: SolanaTx[] = [];
495
+
496
+ let isWrapping: boolean = false;
497
+ const isWrappedInSignedTx = feeRate!=null && feeRate.split("#").length>1;
498
+ if(!isWrappedInSignedTx && swapData.token.equals(SolanaTokens.WSOL_ADDRESS)) {
499
+ const ataAcc = await tryWithRetries<Account>(
500
+ () => this.root.Tokens.getATAOrNull(swapData.offererAta),
501
+ this.retryPolicy
502
+ );
503
+ const balance: bigint = ataAcc==null ? 0n : ataAcc.amount;
504
+
505
+ if(balance < swapData.getAmount()) {
506
+ //Need to wrap more SOL to WSOL
507
+ await this.root.Tokens.Wrap(swapData.offerer, swapData.getAmount() - balance, ataAcc==null)
508
+ .addToTxs(txs, feeRate, block);
509
+ isWrapping = true;
510
+ }
511
+ }
512
+
513
+ const initTx = await (await this.InitPayIn(swapData, BigInt(timeout), feeRate)).tx(feeRate, block);
514
+ initTx.tx.addSignature(swapData.claimer, Buffer.from(signatureStr, "hex"));
515
+ txs.push(initTx);
516
+
517
+ this.logger.debug("txsInitPayIn(): create swap init TX, swap: "+swapData.getClaimHash()+
518
+ " wrapping client-side: "+isWrapping+" feerate: "+feeRate);
519
+
520
+ return txs;
521
+ }
522
+
523
+ /**
524
+ * Creates init transactions (InitNotPayIn) with a valid signature from an intermediary
525
+ *
526
+ * @param swapData swap to initialize
527
+ * @param timeout init signature timeout
528
+ * @param prefix init signature prefix
529
+ * @param signature init signature
530
+ * @param skipChecks whether to skip signature validity checks
531
+ * @param feeRate fee rate to use for the transaction
532
+ */
533
+ public async txsInit(swapData: SolanaSwapData, timeout: string, prefix: string, signature: string, skipChecks?: boolean, feeRate?: string): Promise<SolanaTx[]> {
534
+ if(!skipChecks) {
535
+ await tryWithRetries(
536
+ () => this.isSignatureValid(swapData.getClaimer(), swapData, timeout, prefix, signature, feeRate),
537
+ this.retryPolicy,
538
+ (e) => e instanceof SignatureVerificationError
539
+ );
540
+ }
541
+
542
+ const [slotNumber, signatureStr] = signature.split(";");
543
+ const block = await tryWithRetries(
544
+ () => this.root.Blocks.getParsedBlock(parseInt(slotNumber)),
545
+ this.retryPolicy
546
+ );
547
+
548
+ const initTx = await (await this.InitNotPayIn(swapData, BigInt(timeout))).tx(feeRate, block);
549
+ initTx.tx.addSignature(swapData.offerer, Buffer.from(signatureStr, "hex"));
550
+
551
+ this.logger.debug("txsInit(): create swap init TX, swap: "+swapData.getClaimHash()+" feerate: "+feeRate);
552
+
553
+ return [initTx];
554
+ }
555
+
556
+ /**
557
+ * Returns the fee rate to be used for a specific init transaction, also adding indication whether the WSOL ATA
558
+ * should be initialized in the init transaction and/or current balance in the WSOL ATA
559
+ *
560
+ * @param offerer
561
+ * @param claimer
562
+ * @param token
563
+ * @param paymentHash
564
+ */
565
+ public async getInitPayInFeeRate(offerer?: PublicKey, claimer?: PublicKey, token?: PublicKey, paymentHash?: string): Promise<string> {
566
+ const accounts: PublicKey[] = [];
567
+
568
+ if (offerer != null) accounts.push(offerer);
569
+ if (token != null) {
570
+ accounts.push(this.program.SwapVault(token));
571
+ if (offerer != null) accounts.push(getAssociatedTokenAddressSync(token, offerer));
572
+ if (claimer != null) accounts.push(this.program.SwapUserVault(claimer, token));
573
+ }
574
+ if (paymentHash != null) accounts.push(this.program.SwapEscrowState(Buffer.from(paymentHash, "hex")));
575
+
576
+ const shouldCheckWSOLAta = token != null && offerer != null && token.equals(SolanaTokens.WSOL_ADDRESS);
577
+ let [feeRate, _account] = await Promise.all([
578
+ this.root.Fees.getFeeRate(accounts),
579
+ shouldCheckWSOLAta ?
580
+ this.root.Tokens.getATAOrNull(getAssociatedTokenAddressSync(token, offerer)) :
581
+ Promise.resolve(null)
582
+ ]);
583
+
584
+ if(shouldCheckWSOLAta) {
585
+ const account: Account = _account;
586
+ const balance: bigint = account == null ? 0n : account.amount;
587
+ //Add an indication about whether the ATA is initialized & balance it contains
588
+ feeRate += "#" + (account != null ? "0" : "1") + ";" + balance.toString(10);
589
+ }
590
+
591
+ this.logger.debug("getInitPayInFeeRate(): feerate computed: "+feeRate);
592
+ return feeRate;
593
+ }
594
+
595
+ /**
596
+ * Returns the fee rate to be used for a specific init transaction
597
+ *
598
+ * @param offerer
599
+ * @param claimer
600
+ * @param token
601
+ * @param paymentHash
602
+ */
603
+ public getInitFeeRate(offerer?: PublicKey, claimer?: PublicKey, token?: PublicKey, paymentHash?: string): Promise<string> {
604
+ const accounts: PublicKey[] = [];
605
+
606
+ if(offerer!=null && token!=null) accounts.push(this.program.SwapUserVault(offerer, token));
607
+ if(claimer!=null) accounts.push(claimer)
608
+ if(paymentHash!=null) accounts.push(this.program.SwapEscrowState(Buffer.from(paymentHash, "hex")));
609
+
610
+ return this.root.Fees.getFeeRate(accounts);
611
+ }
612
+
613
+ /**
614
+ * Get the estimated solana fee of the init transaction, this includes the required deposit for creating swap PDA
615
+ * and also deposit for ATAs
616
+ */
617
+ async getInitFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
618
+ if(swapData==null) return BigInt(this.program.ESCROW_STATE_RENT_EXEMPT) + await this.getRawInitFee(swapData, feeRate);
619
+
620
+ feeRate = feeRate ||
621
+ (swapData.payIn
622
+ ? await this.getInitPayInFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash)
623
+ : await this.getInitFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash));
624
+
625
+ const [rawFee, initAta] = await Promise.all([
626
+ this.getRawInitFee(swapData, feeRate),
627
+ swapData!=null && swapData.payOut ?
628
+ this.root.Tokens.getATAOrNull(getAssociatedTokenAddressSync(swapData.token, swapData.claimer)).then(acc => acc==null) :
629
+ Promise.resolve<null>(null)
630
+ ]);
631
+
632
+ let resultingFee = BigInt(this.program.ESCROW_STATE_RENT_EXEMPT) + rawFee;
633
+ if(initAta) resultingFee += BigInt(SolanaTokens.SPL_ATA_RENT_EXEMPT);
634
+
635
+ if(swapData.payIn && this.shouldWrapOnInit(swapData, feeRate) && this.extractAtaDataFromFeeRate(feeRate).initAta) {
636
+ resultingFee += BigInt(SolanaTokens.SPL_ATA_RENT_EXEMPT);
637
+ }
638
+
639
+ return resultingFee;
640
+ }
641
+
642
+ /**
643
+ * Get the estimated solana fee of the init transaction, without the required deposit for creating swap PDA
644
+ */
645
+ async getRawInitFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
646
+ if(swapData==null) return 10000n;
647
+
648
+ feeRate = feeRate ||
649
+ (swapData.payIn
650
+ ? await this.getInitPayInFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash)
651
+ : await this.getInitFeeRate(swapData.offerer, swapData.claimer, swapData.token, swapData.paymentHash));
652
+
653
+ let computeBudget = swapData.payIn ? SwapInit.CUCosts.INIT_PAY_IN : SwapInit.CUCosts.INIT;
654
+ if(swapData.payIn && this.shouldWrapOnInit(swapData, feeRate)) {
655
+ computeBudget += SolanaTokens.CUCosts.WRAP_SOL;
656
+ const data = this.extractAtaDataFromFeeRate(feeRate);
657
+ if(data.initAta) computeBudget += SolanaTokens.CUCosts.ATA_INIT;
658
+ }
659
+ const baseFee = swapData.payIn ? 10000n : 10000n + 5000n;
660
+
661
+ return baseFee + this.root.Fees.getPriorityFee(computeBudget, feeRate);
662
+ }
663
+
664
664
  }