@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,354 +1,354 @@
1
- import {SolanaSwapModule} from "../SolanaSwapModule";
2
- import {SolanaSwapData} from "../SolanaSwapData";
3
- import {sha256} from "@noble/hashes/sha2";
4
- import {sign} from "tweetnacl";
5
- import {SignatureVerificationError, SwapDataVerificationError} from "@atomiqlabs/base";
6
- import {SolanaTx} from "../../chain/modules/SolanaTransactions";
7
- import {
8
- Ed25519Program, Keypair,
9
- PublicKey,
10
- SYSVAR_INSTRUCTIONS_PUBKEY
11
- } from "@solana/web3.js";
12
- import {
13
- getAssociatedTokenAddressSync,
14
- TOKEN_PROGRAM_ID
15
- } from "@solana/spl-token";
16
- import {SolanaAction} from "../../chain/SolanaAction";
17
- import {toBN} from "../../../utils/Utils";
18
- import {Buffer} from "buffer";
19
- import {SolanaSigner} from "../../wallet/SolanaSigner";
20
- import {SolanaTokens} from "../../chain/modules/SolanaTokens";
21
- import * as BN from "bn.js";
22
- import {isSwapProgramV1} from "../SolanaSwapProgram";
23
-
24
- export class SwapRefund extends SolanaSwapModule {
25
-
26
- private static readonly CUCosts = {
27
- REFUND: 15000,
28
- REFUND_PAY_OUT: 50000
29
- };
30
-
31
- /**
32
- * Action for generic Refund instruction
33
- *
34
- * @param signer
35
- * @param swapData
36
- * @param refundAuthTimeout optional refund authorization timeout (should be 0 for refunding expired swaps)
37
- * @private
38
- */
39
- private async Refund(signer: PublicKey, swapData: SolanaSwapData, refundAuthTimeout?: bigint): Promise<SolanaAction> {
40
- const accounts = {
41
- offerer: swapData.offerer,
42
- claimer: swapData.claimer,
43
- escrowState: this.program._SwapEscrowState(Buffer.from(swapData.paymentHash, "hex")),
44
- claimerUserData: !swapData.payOut ? this.program._SwapUserVault(swapData.claimer, swapData.token) : null,
45
- ixSysvar: refundAuthTimeout!=null ? SYSVAR_INSTRUCTIONS_PUBKEY : null
46
- };
47
-
48
- const useTimeout = refundAuthTimeout!=null ? refundAuthTimeout : 0n;
49
- if(swapData.isPayIn()) {
50
- const ata = getAssociatedTokenAddressSync(swapData.token, swapData.offerer);
51
-
52
- return new SolanaAction(signer, this.root,
53
- await this.swapProgram.methods
54
- .offererRefundPayIn(toBN(useTimeout))
55
- .accounts({
56
- ...accounts,
57
- offererAta: ata,
58
- vault: this.program._SwapVault(swapData.token),
59
- vaultAuthority: this.program._SwapVaultAuthority, // Only necessary for V1 program
60
- tokenProgram: TOKEN_PROGRAM_ID
61
- })
62
- .instruction(),
63
- SwapRefund.CUCosts.REFUND_PAY_OUT
64
- );
65
- } else {
66
- return new SolanaAction(signer, this.root,
67
- await this.swapProgram.methods
68
- .offererRefund(toBN(useTimeout))
69
- .accounts({
70
- ...accounts,
71
- offererUserData: this.program._SwapUserVault(swapData.offerer, swapData.token)
72
- })
73
- .instruction(),
74
- SwapRefund.CUCosts.REFUND
75
- );
76
- }
77
- }
78
-
79
- /**
80
- * Action for refunding with signature, adds the Ed25519 verify instruction
81
- *
82
- * @param signer
83
- * @param swapData
84
- * @param timeout
85
- * @param prefix
86
- * @param signature
87
- * @private
88
- */
89
- private async RefundWithSignature(
90
- signer: PublicKey,
91
- swapData: SolanaSwapData,
92
- timeout: string,
93
- prefix: string,
94
- signature: Buffer
95
- ): Promise<SolanaAction> {
96
- const action = new SolanaAction(signer, this.root,
97
- Ed25519Program.createInstructionWithPublicKey({
98
- message: this.getRefundMessage(swapData, prefix, timeout),
99
- publicKey: swapData.claimer.toBuffer(),
100
- signature: signature
101
- }),
102
- 0,
103
- undefined,
104
- undefined,
105
- true
106
- );
107
- action.addAction(await this.Refund(signer, swapData, BigInt(timeout)));
108
- return action;
109
- }
110
-
111
- /**
112
- * Gets the message to be signed as a refund authorization
113
- *
114
- * @param swapData
115
- * @param prefix
116
- * @param timeout
117
- * @private
118
- */
119
- private getRefundMessage(swapData: SolanaSwapData, prefix: string, timeout: string): Buffer {
120
- const messageBuffers = [
121
- Buffer.from(prefix, "ascii"),
122
- swapData.amount.toArrayLike(Buffer, "le", 8),
123
- swapData.expiry.toArrayLike(Buffer, "le", 8),
124
- swapData.sequence.toArrayLike(Buffer, "le", 8),
125
- Buffer.from(swapData.paymentHash, "hex"),
126
- new BN(timeout).toArrayLike(Buffer, "le", 8)
127
- ];
128
-
129
- return Buffer.from(sha256(Buffer.concat(messageBuffers)));
130
- }
131
-
132
- /**
133
- * Checks whether we should unwrap the WSOL to SOL when refunding the swap
134
- *
135
- * @param signer
136
- * @param swapData
137
- * @private
138
- */
139
- private shouldUnwrap(signer: PublicKey, swapData: SolanaSwapData): boolean {
140
- return swapData.isPayIn() &&
141
- swapData.token.equals(SolanaTokens.WSOL_ADDRESS) &&
142
- signer.equals(swapData.offerer);
143
- }
144
-
145
- public signSwapRefund(
146
- signer: SolanaSigner,
147
- swapData: SolanaSwapData,
148
- authorizationTimeout: number
149
- ): Promise<{ prefix: string; timeout: string; signature: string }> {
150
- if(signer.keypair==null) throw new Error("Unsupported");
151
- if(!signer.getPublicKey().equals(swapData.claimer)) throw new Error("Invalid signer, public key mismatch!");
152
-
153
- const authPrefix = "refund";
154
- const authTimeout = Math.floor(Date.now()/1000)+authorizationTimeout;
155
-
156
- const messageBuffer = this.getRefundMessage(swapData, authPrefix, authTimeout.toString(10));
157
- const signature = sign.detached(messageBuffer, signer.keypair.secretKey);
158
-
159
- return Promise.resolve({
160
- prefix: authPrefix,
161
- timeout: authTimeout.toString(10),
162
- signature: Buffer.from(signature).toString("hex")
163
- });
164
- }
165
-
166
- public isSignatureValid(swapData: SolanaSwapData, timeout: string, prefix: string, signature: string): Promise<Buffer> {
167
- if(prefix!=="refund") throw new SignatureVerificationError("Invalid prefix");
168
-
169
- const expiryTimestamp = BigInt(timeout);
170
- const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
171
-
172
- const isExpired = (expiryTimestamp - currentTimestamp) < BigInt(this.program._authGracePeriod);
173
- if(isExpired) throw new SignatureVerificationError("Authorization expired!");
174
-
175
- const signatureBuffer = Buffer.from(signature, "hex");
176
- const messageBuffer = this.getRefundMessage(swapData, prefix, timeout);
177
-
178
- if(!sign.detached.verify(messageBuffer, signatureBuffer, swapData.claimer.toBuffer())) {
179
- throw new SignatureVerificationError("Invalid signature!");
180
- }
181
-
182
- return Promise.resolve(messageBuffer);
183
- }
184
-
185
- /**
186
- * Creates transactions required for refunding timed out swap, also unwraps WSOL to SOL
187
- *
188
- * @param signer
189
- * @param swapData swap data to refund
190
- * @param check whether to check if swap is already expired and refundable
191
- * @param initAta should initialize ATA if it doesn't exist
192
- * @param feeRate fee rate to be used for the transactions
193
- */
194
- public async txsRefund(
195
- signer: PublicKey,
196
- swapData: SolanaSwapData,
197
- check?: boolean,
198
- initAta?: boolean,
199
- feeRate?: string
200
- ): Promise<SolanaTx[]> {
201
- if(isSwapProgramV1(this.program.program)) {
202
- //V1 only allows the offerer to refund
203
- if(!swapData.isOfferer(signer.toString())) throw new Error("Only offerer can refund in V1 on Solana");
204
- }
205
-
206
- if(check && !await this.program.isRequestRefundable(swapData.offerer.toString(), swapData)) {
207
- throw new SwapDataVerificationError("Not refundable yet!");
208
- }
209
-
210
- let shouldInitAta = false;
211
- if(swapData.isPayIn()) {
212
- if(swapData.offererAta==null) throw new Error("Swap data offererAta is null for payIn swap!");
213
- if(!await this.root.Tokens.ataExists(swapData.offererAta)) {
214
- if(!initAta) throw new SwapDataVerificationError("ATA not initialized");
215
- shouldInitAta = true;
216
- }
217
- }
218
-
219
- if(feeRate==null) feeRate = await this.program.getRefundFeeRate(swapData)
220
-
221
- const shouldUnwrap = this.shouldUnwrap(signer, swapData);
222
- const action = new SolanaAction(signer, this.root);
223
- if(shouldInitAta) {
224
- const initAction = this.root.Tokens.InitAta(signer, swapData.offerer, swapData.token, swapData.offererAta);
225
- if(initAction==null) throw new SwapDataVerificationError("Invalid claimer token account address")
226
- action.addAction(initAction);
227
- }
228
- action.add(await this.Refund(signer, swapData));
229
- if(shouldUnwrap) action.add(this.root.Tokens.Unwrap(signer));
230
-
231
- this.logger.debug("txsRefund(): creating claim transaction, swap: "+swapData.getClaimHash()+
232
- " initializingAta: "+shouldInitAta+" unwrapping: "+shouldUnwrap);
233
-
234
- return [await action.tx(feeRate)];
235
- }
236
-
237
- /**
238
- * Creates transactions required for refunding the swap with authorization signature, also unwraps WSOL to SOL
239
- *
240
- * @param signer
241
- * @param swapData swap data to refund
242
- * @param timeout signature timeout
243
- * @param prefix signature prefix of the counterparty
244
- * @param signature signature of the counterparty
245
- * @param check whether to check if swap is committed before attempting refund
246
- * @param initAta should initialize ATA if it doesn't exist
247
- * @param feeRate fee rate to be used for the transactions
248
- */
249
- public async txsRefundWithAuthorization(
250
- signer: PublicKey,
251
- swapData: SolanaSwapData,
252
- timeout: string,
253
- prefix: string,
254
- signature: string,
255
- check?: boolean,
256
- initAta?: boolean,
257
- feeRate?: string
258
- ): Promise<SolanaTx[]> {
259
- if(isSwapProgramV1(this.program.program)) {
260
- //V1 only allows the offerer to refund
261
- if(!swapData.isOfferer(signer.toString())) throw new Error("Only offerer can refund in V1 on Solana");
262
- }
263
-
264
- if(check && !await this.program.isCommited(swapData)) {
265
- throw new SwapDataVerificationError("Not correctly committed");
266
- }
267
- await this.isSignatureValid(swapData, timeout, prefix, signature);
268
-
269
- let shouldInitAta = false;
270
- if(swapData.isPayIn()) {
271
- if(swapData.offererAta==null) throw new Error("Swap data offererAta is null for payIn swap!");
272
- if(!await this.root.Tokens.ataExists(swapData.offererAta)) {
273
- if(!initAta) throw new SwapDataVerificationError("ATA not initialized");
274
- shouldInitAta = true;
275
- }
276
- }
277
-
278
- if(feeRate==null) feeRate = await this.program.getRefundFeeRate(swapData);
279
-
280
- const signatureBuffer = Buffer.from(signature, "hex");
281
-
282
- const shouldUnwrap = this.shouldUnwrap(signer, swapData);
283
- const action = await this.RefundWithSignature(signer, swapData, timeout, prefix, signatureBuffer);
284
- if(shouldInitAta) {
285
- const initAction = this.root.Tokens.InitAta(signer, swapData.offerer, swapData.token, swapData.offererAta);
286
- if(initAction==null) throw new SwapDataVerificationError("Invalid claimer token account address");
287
- action.addAction(initAction, 1); //Need to add it after the Ed25519 verify IX, but before the actual refund IX
288
- }
289
- if(shouldUnwrap) action.add(this.root.Tokens.Unwrap(signer));
290
-
291
- this.logger.debug("txsRefundWithAuthorization(): creating claim transaction, swap: "+swapData.getClaimHash()+
292
- " initializingAta: "+shouldInitAta+" unwrapping: "+shouldUnwrap+
293
- " auth expiry: "+timeout+" signature: "+signature);
294
-
295
- //Push a random keypair to the TX signer such that pesky Phantom
296
- // doesn't fuck up the instructions order!
297
- const tx = await action.tx(feeRate);
298
- const _signer = Keypair.generate();
299
- const ix = tx.tx.instructions.find(val => val.programId.equals(this.program.program.programId));
300
- if(ix!=null){
301
- ix.keys.push({
302
- pubkey: _signer.publicKey,
303
- isSigner: true,
304
- isWritable: false
305
- });
306
- (tx.signers ??= []).push(_signer);
307
- }
308
-
309
- return [tx];
310
- }
311
-
312
- public getRefundFeeRate(swapData: SolanaSwapData): Promise<string> {
313
- const accounts: PublicKey[] = [];
314
- if(swapData.payIn) {
315
- if(swapData.token!=null) accounts.push(this.program._SwapVault(swapData.token));
316
- if(swapData.offerer!=null) accounts.push(swapData.offerer);
317
- if(swapData.claimer!=null) accounts.push(swapData.claimer);
318
- if(swapData.offererAta!=null && !swapData.offererAta.equals(PublicKey.default)) accounts.push(swapData.offererAta);
319
- } else {
320
- if(swapData.offerer!=null) {
321
- accounts.push(swapData.offerer);
322
- if(swapData.token!=null) accounts.push(this.program._SwapUserVault(swapData.offerer, swapData.token));
323
- }
324
- if(swapData.claimer!=null) accounts.push(swapData.claimer);
325
- }
326
-
327
- if(swapData.paymentHash!=null) accounts.push(this.program._SwapEscrowState(Buffer.from(swapData.paymentHash, "hex")));
328
-
329
- return this.root.Fees.getFeeRate(accounts);
330
- }
331
-
332
- /**
333
- * Get the estimated solana transaction fee of the refund transaction, in the worst case scenario in case where the
334
- * ATA needs to be initialized again (i.e. adding the ATA rent exempt lamports to the fee)
335
- */
336
- async getRefundFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
337
- return BigInt(swapData==null || swapData.payIn ? SolanaTokens.SPL_ATA_RENT_EXEMPT : 0) +
338
- await this.getRawRefundFee(swapData, feeRate);
339
- }
340
-
341
- /**
342
- * Get the estimated solana transaction fee of the refund transaction
343
- */
344
- async getRawRefundFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
345
- if(swapData==null) return 15000n;
346
-
347
- feeRate = feeRate || await this.getRefundFeeRate(swapData);
348
-
349
- const computeBudget = swapData.payIn ? SwapRefund.CUCosts.REFUND_PAY_OUT : SwapRefund.CUCosts.REFUND;
350
-
351
- return 15000n + this.root.Fees.getPriorityFee(computeBudget, feeRate);
352
- }
353
-
1
+ import {SolanaSwapModule} from "../SolanaSwapModule";
2
+ import {SolanaSwapData} from "../SolanaSwapData";
3
+ import {sha256} from "@noble/hashes/sha2";
4
+ import {sign} from "tweetnacl";
5
+ import {SignatureVerificationError, SwapDataVerificationError} from "@atomiqlabs/base";
6
+ import {SolanaTx} from "../../chain/modules/SolanaTransactions";
7
+ import {
8
+ Ed25519Program, Keypair,
9
+ PublicKey,
10
+ SYSVAR_INSTRUCTIONS_PUBKEY
11
+ } from "@solana/web3.js";
12
+ import {
13
+ getAssociatedTokenAddressSync,
14
+ TOKEN_PROGRAM_ID
15
+ } from "@solana/spl-token";
16
+ import {SolanaAction} from "../../chain/SolanaAction";
17
+ import {toBN} from "../../../utils/Utils";
18
+ import {Buffer} from "buffer";
19
+ import {SolanaSigner} from "../../wallet/SolanaSigner";
20
+ import {SolanaTokens} from "../../chain/modules/SolanaTokens";
21
+ import * as BN from "bn.js";
22
+ import {isSwapProgramV1} from "../SolanaSwapProgram";
23
+
24
+ export class SwapRefund extends SolanaSwapModule {
25
+
26
+ private static readonly CUCosts = {
27
+ REFUND: 15000,
28
+ REFUND_PAY_OUT: 50000
29
+ };
30
+
31
+ /**
32
+ * Action for generic Refund instruction
33
+ *
34
+ * @param signer
35
+ * @param swapData
36
+ * @param refundAuthTimeout optional refund authorization timeout (should be 0 for refunding expired swaps)
37
+ * @private
38
+ */
39
+ private async Refund(signer: PublicKey, swapData: SolanaSwapData, refundAuthTimeout?: bigint): Promise<SolanaAction> {
40
+ const accounts = {
41
+ offerer: swapData.offerer,
42
+ claimer: swapData.claimer,
43
+ escrowState: this.program._SwapEscrowState(Buffer.from(swapData.paymentHash, "hex")),
44
+ claimerUserData: !swapData.payOut ? this.program._SwapUserVault(swapData.claimer, swapData.token) : null,
45
+ ixSysvar: refundAuthTimeout!=null ? SYSVAR_INSTRUCTIONS_PUBKEY : null
46
+ };
47
+
48
+ const useTimeout = refundAuthTimeout!=null ? refundAuthTimeout : 0n;
49
+ if(swapData.isPayIn()) {
50
+ const ata = getAssociatedTokenAddressSync(swapData.token, swapData.offerer);
51
+
52
+ return new SolanaAction(signer, this.root,
53
+ await this.swapProgram.methods
54
+ .offererRefundPayIn(toBN(useTimeout))
55
+ .accounts({
56
+ ...accounts,
57
+ offererAta: ata,
58
+ vault: this.program._SwapVault(swapData.token),
59
+ vaultAuthority: this.program._SwapVaultAuthority, // Only necessary for V1 program
60
+ tokenProgram: TOKEN_PROGRAM_ID
61
+ })
62
+ .instruction(),
63
+ SwapRefund.CUCosts.REFUND_PAY_OUT
64
+ );
65
+ } else {
66
+ return new SolanaAction(signer, this.root,
67
+ await this.swapProgram.methods
68
+ .offererRefund(toBN(useTimeout))
69
+ .accounts({
70
+ ...accounts,
71
+ offererUserData: this.program._SwapUserVault(swapData.offerer, swapData.token)
72
+ })
73
+ .instruction(),
74
+ SwapRefund.CUCosts.REFUND
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Action for refunding with signature, adds the Ed25519 verify instruction
81
+ *
82
+ * @param signer
83
+ * @param swapData
84
+ * @param timeout
85
+ * @param prefix
86
+ * @param signature
87
+ * @private
88
+ */
89
+ private async RefundWithSignature(
90
+ signer: PublicKey,
91
+ swapData: SolanaSwapData,
92
+ timeout: string,
93
+ prefix: string,
94
+ signature: Buffer
95
+ ): Promise<SolanaAction> {
96
+ const action = new SolanaAction(signer, this.root,
97
+ Ed25519Program.createInstructionWithPublicKey({
98
+ message: this.getRefundMessage(swapData, prefix, timeout),
99
+ publicKey: swapData.claimer.toBuffer(),
100
+ signature: signature
101
+ }),
102
+ 0,
103
+ undefined,
104
+ undefined,
105
+ true
106
+ );
107
+ action.addAction(await this.Refund(signer, swapData, BigInt(timeout)));
108
+ return action;
109
+ }
110
+
111
+ /**
112
+ * Gets the message to be signed as a refund authorization
113
+ *
114
+ * @param swapData
115
+ * @param prefix
116
+ * @param timeout
117
+ * @private
118
+ */
119
+ private getRefundMessage(swapData: SolanaSwapData, prefix: string, timeout: string): Buffer {
120
+ const messageBuffers = [
121
+ Buffer.from(prefix, "ascii"),
122
+ swapData.amount.toArrayLike(Buffer, "le", 8),
123
+ swapData.expiry.toArrayLike(Buffer, "le", 8),
124
+ swapData.sequence.toArrayLike(Buffer, "le", 8),
125
+ Buffer.from(swapData.paymentHash, "hex"),
126
+ new BN(timeout).toArrayLike(Buffer, "le", 8)
127
+ ];
128
+
129
+ return Buffer.from(sha256(Buffer.concat(messageBuffers)));
130
+ }
131
+
132
+ /**
133
+ * Checks whether we should unwrap the WSOL to SOL when refunding the swap
134
+ *
135
+ * @param signer
136
+ * @param swapData
137
+ * @private
138
+ */
139
+ private shouldUnwrap(signer: PublicKey, swapData: SolanaSwapData): boolean {
140
+ return swapData.isPayIn() &&
141
+ swapData.token.equals(SolanaTokens.WSOL_ADDRESS) &&
142
+ signer.equals(swapData.offerer);
143
+ }
144
+
145
+ public signSwapRefund(
146
+ signer: SolanaSigner,
147
+ swapData: SolanaSwapData,
148
+ authorizationTimeout: number
149
+ ): Promise<{ prefix: string; timeout: string; signature: string }> {
150
+ if(signer.keypair==null) throw new Error("Unsupported");
151
+ if(!signer.getPublicKey().equals(swapData.claimer)) throw new Error("Invalid signer, public key mismatch!");
152
+
153
+ const authPrefix = "refund";
154
+ const authTimeout = Math.floor(Date.now()/1000)+authorizationTimeout;
155
+
156
+ const messageBuffer = this.getRefundMessage(swapData, authPrefix, authTimeout.toString(10));
157
+ const signature = sign.detached(messageBuffer, signer.keypair.secretKey);
158
+
159
+ return Promise.resolve({
160
+ prefix: authPrefix,
161
+ timeout: authTimeout.toString(10),
162
+ signature: Buffer.from(signature).toString("hex")
163
+ });
164
+ }
165
+
166
+ public isSignatureValid(swapData: SolanaSwapData, timeout: string, prefix: string, signature: string): Promise<Buffer> {
167
+ if(prefix!=="refund") throw new SignatureVerificationError("Invalid prefix");
168
+
169
+ const expiryTimestamp = BigInt(timeout);
170
+ const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
171
+
172
+ const isExpired = (expiryTimestamp - currentTimestamp) < BigInt(this.program._authGracePeriod);
173
+ if(isExpired) throw new SignatureVerificationError("Authorization expired!");
174
+
175
+ const signatureBuffer = Buffer.from(signature, "hex");
176
+ const messageBuffer = this.getRefundMessage(swapData, prefix, timeout);
177
+
178
+ if(!sign.detached.verify(messageBuffer, signatureBuffer, swapData.claimer.toBuffer())) {
179
+ throw new SignatureVerificationError("Invalid signature!");
180
+ }
181
+
182
+ return Promise.resolve(messageBuffer);
183
+ }
184
+
185
+ /**
186
+ * Creates transactions required for refunding timed out swap, also unwraps WSOL to SOL
187
+ *
188
+ * @param signer
189
+ * @param swapData swap data to refund
190
+ * @param check whether to check if swap is already expired and refundable
191
+ * @param initAta should initialize ATA if it doesn't exist
192
+ * @param feeRate fee rate to be used for the transactions
193
+ */
194
+ public async txsRefund(
195
+ signer: PublicKey,
196
+ swapData: SolanaSwapData,
197
+ check?: boolean,
198
+ initAta?: boolean,
199
+ feeRate?: string
200
+ ): Promise<SolanaTx[]> {
201
+ if(isSwapProgramV1(this.program.program)) {
202
+ //V1 only allows the offerer to refund
203
+ if(!swapData.isOfferer(signer.toString())) throw new Error("Only offerer can refund in V1 on Solana");
204
+ }
205
+
206
+ if(check && !await this.program.isRequestRefundable(swapData.offerer.toString(), swapData)) {
207
+ throw new SwapDataVerificationError("Not refundable yet!");
208
+ }
209
+
210
+ let shouldInitAta = false;
211
+ if(swapData.isPayIn()) {
212
+ if(swapData.offererAta==null) throw new Error("Swap data offererAta is null for payIn swap!");
213
+ if(!await this.root.Tokens.ataExists(swapData.offererAta)) {
214
+ if(!initAta) throw new SwapDataVerificationError("ATA not initialized");
215
+ shouldInitAta = true;
216
+ }
217
+ }
218
+
219
+ if(feeRate==null) feeRate = await this.program.getRefundFeeRate(swapData)
220
+
221
+ const shouldUnwrap = this.shouldUnwrap(signer, swapData);
222
+ const action = new SolanaAction(signer, this.root);
223
+ if(shouldInitAta) {
224
+ const initAction = this.root.Tokens.InitAta(signer, swapData.offerer, swapData.token, swapData.offererAta);
225
+ if(initAction==null) throw new SwapDataVerificationError("Invalid claimer token account address")
226
+ action.addAction(initAction);
227
+ }
228
+ action.add(await this.Refund(signer, swapData));
229
+ if(shouldUnwrap) action.add(this.root.Tokens.Unwrap(signer));
230
+
231
+ this.logger.debug("txsRefund(): creating claim transaction, swap: "+swapData.getClaimHash()+
232
+ " initializingAta: "+shouldInitAta+" unwrapping: "+shouldUnwrap);
233
+
234
+ return [await action.tx(feeRate)];
235
+ }
236
+
237
+ /**
238
+ * Creates transactions required for refunding the swap with authorization signature, also unwraps WSOL to SOL
239
+ *
240
+ * @param signer
241
+ * @param swapData swap data to refund
242
+ * @param timeout signature timeout
243
+ * @param prefix signature prefix of the counterparty
244
+ * @param signature signature of the counterparty
245
+ * @param check whether to check if swap is committed before attempting refund
246
+ * @param initAta should initialize ATA if it doesn't exist
247
+ * @param feeRate fee rate to be used for the transactions
248
+ */
249
+ public async txsRefundWithAuthorization(
250
+ signer: PublicKey,
251
+ swapData: SolanaSwapData,
252
+ timeout: string,
253
+ prefix: string,
254
+ signature: string,
255
+ check?: boolean,
256
+ initAta?: boolean,
257
+ feeRate?: string
258
+ ): Promise<SolanaTx[]> {
259
+ if(isSwapProgramV1(this.program.program)) {
260
+ //V1 only allows the offerer to refund
261
+ if(!swapData.isOfferer(signer.toString())) throw new Error("Only offerer can refund in V1 on Solana");
262
+ }
263
+
264
+ if(check && !await this.program.isCommited(swapData)) {
265
+ throw new SwapDataVerificationError("Not correctly committed");
266
+ }
267
+ await this.isSignatureValid(swapData, timeout, prefix, signature);
268
+
269
+ let shouldInitAta = false;
270
+ if(swapData.isPayIn()) {
271
+ if(swapData.offererAta==null) throw new Error("Swap data offererAta is null for payIn swap!");
272
+ if(!await this.root.Tokens.ataExists(swapData.offererAta)) {
273
+ if(!initAta) throw new SwapDataVerificationError("ATA not initialized");
274
+ shouldInitAta = true;
275
+ }
276
+ }
277
+
278
+ if(feeRate==null) feeRate = await this.program.getRefundFeeRate(swapData);
279
+
280
+ const signatureBuffer = Buffer.from(signature, "hex");
281
+
282
+ const shouldUnwrap = this.shouldUnwrap(signer, swapData);
283
+ const action = await this.RefundWithSignature(signer, swapData, timeout, prefix, signatureBuffer);
284
+ if(shouldInitAta) {
285
+ const initAction = this.root.Tokens.InitAta(signer, swapData.offerer, swapData.token, swapData.offererAta);
286
+ if(initAction==null) throw new SwapDataVerificationError("Invalid claimer token account address");
287
+ action.addAction(initAction, 1); //Need to add it after the Ed25519 verify IX, but before the actual refund IX
288
+ }
289
+ if(shouldUnwrap) action.add(this.root.Tokens.Unwrap(signer));
290
+
291
+ this.logger.debug("txsRefundWithAuthorization(): creating claim transaction, swap: "+swapData.getClaimHash()+
292
+ " initializingAta: "+shouldInitAta+" unwrapping: "+shouldUnwrap+
293
+ " auth expiry: "+timeout+" signature: "+signature);
294
+
295
+ //Push a random keypair to the TX signer such that pesky Phantom
296
+ // doesn't fuck up the instructions order!
297
+ const tx = await action.tx(feeRate);
298
+ const _signer = Keypair.generate();
299
+ const ix = tx.tx.instructions.find(val => val.programId.equals(this.program.program.programId));
300
+ if(ix!=null){
301
+ ix.keys.push({
302
+ pubkey: _signer.publicKey,
303
+ isSigner: true,
304
+ isWritable: false
305
+ });
306
+ (tx.signers ??= []).push(_signer);
307
+ }
308
+
309
+ return [tx];
310
+ }
311
+
312
+ public getRefundFeeRate(swapData: SolanaSwapData): Promise<string> {
313
+ const accounts: PublicKey[] = [];
314
+ if(swapData.payIn) {
315
+ if(swapData.token!=null) accounts.push(this.program._SwapVault(swapData.token));
316
+ if(swapData.offerer!=null) accounts.push(swapData.offerer);
317
+ if(swapData.claimer!=null) accounts.push(swapData.claimer);
318
+ if(swapData.offererAta!=null && !swapData.offererAta.equals(PublicKey.default)) accounts.push(swapData.offererAta);
319
+ } else {
320
+ if(swapData.offerer!=null) {
321
+ accounts.push(swapData.offerer);
322
+ if(swapData.token!=null) accounts.push(this.program._SwapUserVault(swapData.offerer, swapData.token));
323
+ }
324
+ if(swapData.claimer!=null) accounts.push(swapData.claimer);
325
+ }
326
+
327
+ if(swapData.paymentHash!=null) accounts.push(this.program._SwapEscrowState(Buffer.from(swapData.paymentHash, "hex")));
328
+
329
+ return this.root.Fees.getFeeRate(accounts);
330
+ }
331
+
332
+ /**
333
+ * Get the estimated solana transaction fee of the refund transaction, in the worst case scenario in case where the
334
+ * ATA needs to be initialized again (i.e. adding the ATA rent exempt lamports to the fee)
335
+ */
336
+ async getRefundFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
337
+ return BigInt(swapData==null || swapData.payIn ? SolanaTokens.SPL_ATA_RENT_EXEMPT : 0) +
338
+ await this.getRawRefundFee(swapData, feeRate);
339
+ }
340
+
341
+ /**
342
+ * Get the estimated solana transaction fee of the refund transaction
343
+ */
344
+ async getRawRefundFee(swapData: SolanaSwapData, feeRate?: string): Promise<bigint> {
345
+ if(swapData==null) return 15000n;
346
+
347
+ feeRate = feeRate || await this.getRefundFeeRate(swapData);
348
+
349
+ const computeBudget = swapData.payIn ? SwapRefund.CUCosts.REFUND_PAY_OUT : SwapRefund.CUCosts.REFUND;
350
+
351
+ return 15000n + this.root.Fees.getPriorityFee(computeBudget, feeRate);
352
+ }
353
+
354
354
  }