@atomiqlabs/chain-solana 13.5.13 → 13.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +73 -73
- package/dist/index.d.ts +81 -81
- package/dist/index.js +102 -102
- package/dist/node/index.d.ts +9 -9
- package/dist/node/index.js +13 -13
- package/dist/solana/SolanaChainType.d.ts +15 -15
- package/dist/solana/SolanaChainType.js +2 -2
- package/dist/solana/SolanaChains.d.ts +12 -12
- package/dist/solana/SolanaChains.js +45 -45
- package/dist/solana/SolanaInitializer.d.ts +94 -94
- package/dist/solana/SolanaInitializer.js +174 -174
- package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +222 -222
- package/dist/solana/btcrelay/SolanaBtcRelay.js +455 -455
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +84 -84
- package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +70 -70
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +92 -92
- package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +109 -109
- package/dist/solana/btcrelay/program/programIdl.json +671 -671
- package/dist/solana/chain/SolanaAction.d.ts +26 -26
- package/dist/solana/chain/SolanaAction.js +87 -87
- package/dist/solana/chain/SolanaChainInterface.d.ts +224 -224
- package/dist/solana/chain/SolanaChainInterface.js +275 -275
- package/dist/solana/chain/SolanaModule.d.ts +14 -14
- package/dist/solana/chain/SolanaModule.js +13 -13
- package/dist/solana/chain/modules/SolanaAddresses.d.ts +8 -8
- package/dist/solana/chain/modules/SolanaAddresses.js +22 -22
- package/dist/solana/chain/modules/SolanaBlocks.d.ts +32 -32
- package/dist/solana/chain/modules/SolanaBlocks.js +78 -78
- package/dist/solana/chain/modules/SolanaEvents.d.ts +68 -68
- package/dist/solana/chain/modules/SolanaEvents.js +238 -238
- package/dist/solana/chain/modules/SolanaFees.d.ts +189 -189
- package/dist/solana/chain/modules/SolanaFees.js +434 -434
- package/dist/solana/chain/modules/SolanaSignatures.d.ts +24 -24
- package/dist/solana/chain/modules/SolanaSignatures.js +39 -39
- package/dist/solana/chain/modules/SolanaSlots.d.ts +33 -33
- package/dist/solana/chain/modules/SolanaSlots.js +72 -72
- package/dist/solana/chain/modules/SolanaTokens.d.ts +123 -123
- package/dist/solana/chain/modules/SolanaTokens.js +242 -242
- package/dist/solana/chain/modules/SolanaTransactions.d.ts +149 -149
- package/dist/solana/chain/modules/SolanaTransactions.js +445 -445
- package/dist/solana/connection/ConnectionWithRetries.d.ts +35 -35
- package/dist/solana/connection/ConnectionWithRetries.js +86 -71
- package/dist/solana/events/SolanaChainEvents.d.ts +45 -45
- package/dist/solana/events/SolanaChainEvents.js +108 -108
- package/dist/solana/events/SolanaChainEventsBrowser.d.ts +205 -205
- package/dist/solana/events/SolanaChainEventsBrowser.js +404 -404
- package/dist/solana/program/SolanaProgramBase.d.ts +73 -73
- package/dist/solana/program/SolanaProgramBase.js +54 -54
- package/dist/solana/program/SolanaProgramModule.d.ts +8 -8
- package/dist/solana/program/SolanaProgramModule.js +11 -11
- package/dist/solana/program/modules/SolanaProgramEvents.d.ts +53 -53
- package/dist/solana/program/modules/SolanaProgramEvents.js +117 -117
- package/dist/solana/swaps/SolanaSwapData.d.ts +333 -333
- package/dist/solana/swaps/SolanaSwapData.js +535 -535
- package/dist/solana/swaps/SolanaSwapModule.d.ts +11 -11
- package/dist/solana/swaps/SolanaSwapModule.js +12 -12
- package/dist/solana/swaps/SolanaSwapProgram.d.ts +376 -376
- package/dist/solana/swaps/SolanaSwapProgram.js +769 -769
- package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -11
- package/dist/solana/swaps/SwapTypeEnum.js +43 -43
- package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +95 -95
- package/dist/solana/swaps/modules/SolanaDataAccount.js +232 -232
- package/dist/solana/swaps/modules/SolanaLpVault.d.ts +69 -69
- package/dist/solana/swaps/modules/SolanaLpVault.js +171 -171
- package/dist/solana/swaps/modules/SwapClaim.d.ts +126 -126
- package/dist/solana/swaps/modules/SwapClaim.js +294 -294
- package/dist/solana/swaps/modules/SwapInit.d.ts +213 -213
- package/dist/solana/swaps/modules/SwapInit.js +658 -658
- package/dist/solana/swaps/modules/SwapRefund.d.ts +87 -87
- package/dist/solana/swaps/modules/SwapRefund.js +293 -293
- package/dist/solana/swaps/programIdl.json +945 -945
- package/dist/solana/swaps/programTypes.d.ts +943 -943
- package/dist/solana/swaps/programTypes.js +945 -945
- package/dist/solana/swaps/v1/programIdl.json +945 -945
- package/dist/solana/swaps/v1/programTypes.d.ts +943 -943
- package/dist/solana/swaps/v1/programTypes.js +945 -945
- package/dist/solana/swaps/v2/programIdl.json +952 -952
- package/dist/solana/swaps/v2/programTypes.d.ts +950 -950
- package/dist/solana/swaps/v2/programTypes.js +952 -952
- package/dist/solana/wallet/SolanaKeypairWallet.d.ts +29 -29
- package/dist/solana/wallet/SolanaKeypairWallet.js +50 -50
- package/dist/solana/wallet/SolanaSigner.d.ts +30 -30
- package/dist/solana/wallet/SolanaSigner.js +30 -30
- package/dist/utils/Utils.d.ts +58 -58
- package/dist/utils/Utils.js +170 -170
- package/node/index.d.ts +1 -1
- package/node/index.js +3 -3
- package/package.json +46 -46
- package/src/index.ts +87 -87
- package/src/node/index.ts +9 -9
- package/src/solana/SolanaChainType.ts +32 -32
- package/src/solana/SolanaChains.ts +46 -46
- package/src/solana/SolanaInitializer.ts +278 -278
- package/src/solana/btcrelay/SolanaBtcRelay.ts +615 -615
- package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +116 -116
- package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +148 -148
- package/src/solana/btcrelay/program/programIdl.json +670 -670
- package/src/solana/chain/SolanaAction.ts +109 -109
- package/src/solana/chain/SolanaChainInterface.ts +404 -404
- package/src/solana/chain/SolanaModule.ts +20 -20
- package/src/solana/chain/modules/SolanaAddresses.ts +20 -20
- package/src/solana/chain/modules/SolanaBlocks.ts +89 -89
- package/src/solana/chain/modules/SolanaEvents.ts +271 -271
- package/src/solana/chain/modules/SolanaFees.ts +522 -522
- package/src/solana/chain/modules/SolanaSignatures.ts +39 -39
- package/src/solana/chain/modules/SolanaSlots.ts +85 -85
- package/src/solana/chain/modules/SolanaTokens.ts +300 -300
- package/src/solana/chain/modules/SolanaTransactions.ts +503 -503
- package/src/solana/connection/ConnectionWithRetries.ts +113 -96
- package/src/solana/events/SolanaChainEvents.ts +127 -127
- package/src/solana/events/SolanaChainEventsBrowser.ts +495 -495
- package/src/solana/program/SolanaProgramBase.ts +119 -119
- package/src/solana/program/SolanaProgramModule.ts +15 -15
- package/src/solana/program/modules/SolanaProgramEvents.ts +157 -157
- package/src/solana/swaps/SolanaSwapData.ts +735 -735
- package/src/solana/swaps/SolanaSwapModule.ts +19 -19
- package/src/solana/swaps/SolanaSwapProgram.ts +1074 -1074
- package/src/solana/swaps/SwapTypeEnum.ts +30 -30
- package/src/solana/swaps/modules/SolanaDataAccount.ts +302 -302
- package/src/solana/swaps/modules/SolanaLpVault.ts +208 -208
- package/src/solana/swaps/modules/SwapClaim.ts +387 -387
- package/src/solana/swaps/modules/SwapInit.ts +785 -785
- package/src/solana/swaps/modules/SwapRefund.ts +353 -353
- package/src/solana/swaps/v1/programIdl.json +944 -944
- package/src/solana/swaps/v1/programTypes.ts +1885 -1885
- package/src/solana/swaps/v2/programIdl.json +951 -951
- package/src/solana/swaps/v2/programTypes.ts +1899 -1899
- package/src/solana/wallet/SolanaKeypairWallet.ts +56 -56
- package/src/solana/wallet/SolanaSigner.ts +43 -43
- package/src/utils/Utils.ts +194 -194
|
@@ -1,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
|
}
|