@atomiqlabs/chain-solana 13.5.13 → 13.5.14

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