1
1
import logging
2
2
import base64
3
- from typing import Optional
3
+ from typing import Optional , List
4
4
import httpx
5
5
6
6
from solders .pubkey import Pubkey
7
+ from solana .rpc .commitment import Finalized
7
8
from solders .transaction import VersionedTransaction
8
- from solders .message import to_bytes_versioned
9
- from solders .null_signer import NullSigner
9
+ from solders .message import (
10
+ to_bytes_versioned ,
11
+ MessageV0 ,
12
+ )
13
+ from spl .token .instructions import (
14
+ transfer_checked as spl_transfer ,
15
+ TransferCheckedParams as SPLTransferParams ,
16
+ )
10
17
from spl .token .async_client import AsyncToken
18
+ from solders .address_lookup_table_account import AddressLookupTableAccount
19
+ from solders .null_signer import NullSigner
20
+ from solders .hash import Hash
21
+ from solders .instruction import Instruction , AccountMeta
22
+ from solders .compute_budget import set_compute_unit_limit , set_compute_unit_price
23
+ from solders .signature import Signature
11
24
from sakit .utils .wallet import SolanaWalletClient
25
+ from sakit .utils .transfer import transfer , TransferParams
12
26
13
27
JUP_API = "https://quote-api.jup.ag/v6"
14
28
SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
15
29
TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
16
-
30
+ LAMPORTS_PER_SOL = 10 ** 9
17
31
18
32
class TradeManager :
33
+ @staticmethod
34
+ def parse_address_table_lookups (addresses ):
35
+ if not addresses :
36
+ return []
37
+ if isinstance (addresses , str ):
38
+ addresses = [addresses ]
39
+ # Use empty addresses for simulation/compilation
40
+ return [AddressLookupTableAccount (Pubkey .from_string (addr ), []) for addr in addresses ]
41
+
42
+ @staticmethod
43
+ def parse_instruction (ix_obj ) -> Instruction :
44
+ program_id = Pubkey .from_string (ix_obj ["programId" ])
45
+ accounts = [
46
+ AccountMeta (
47
+ pubkey = Pubkey .from_string (acc ["pubkey" ]),
48
+ is_signer = acc ["isSigner" ],
49
+ is_writable = acc ["isWritable" ],
50
+ )
51
+ for acc in ix_obj ["accounts" ]
52
+ ]
53
+ data = base64 .b64decode (ix_obj ["data" ])
54
+ return Instruction (program_id = program_id , accounts = accounts , data = data )
55
+
56
+ @staticmethod
57
+ def parse_instruction_list (ix_list ) -> List [Instruction ]:
58
+ return [TradeManager .parse_instruction (ix ) for ix in ix_list ]
59
+
19
60
@staticmethod
20
61
async def trade (
21
62
wallet : SolanaWalletClient ,
@@ -25,34 +66,15 @@ async def trade(
25
66
slippage_bps : int = 300 ,
26
67
jupiter_url : Optional [str ] = None ,
27
68
no_signer : bool = False ,
69
+ provider : Optional [str ] = None ,
70
+ fee_percentage : float = 0.85 ,
28
71
) -> VersionedTransaction :
29
72
"""
30
- Swap tokens using Jupiter Exchange.
31
-
32
- Args:
33
- wallet: SolanaWalletClient instance.
34
- output_mint (str): Target token mint address.
35
- input_amount (float): Amount to swap.
36
- input_mint (str): Source token mint address (default: SOL).
37
- slippage_bps (int): Slippage tolerance in basis points (default: 300 = 3%).
38
- jupiter_url (str): Jupiter API base URL.
39
- no_signer (bool): If True, does not sign the transaction with the wallet's keypair.
40
-
41
- Returns:
42
- VersionedTransaction: Signed transaction ready for submission.
43
-
44
- Raises:
45
- Exception: If the swap fails.
73
+ Swap tokens using Jupiter Exchange, with compute budget and optional priority fee (if provider == 'helius').
46
74
"""
47
75
try :
48
- if (
49
- input_mint is None
50
- or input_mint == "So11111111111111111111111111111111111111112"
51
- ):
52
- # Default to SOL
53
- input_mint = "So11111111111111111111111111111111111111112"
54
- adjusted_amount = int (input_amount * (10 ** 9 )) # SOL has 9 decimals
55
-
76
+ if input_mint == "So11111111111111111111111111111111111111112" :
77
+ adjusted_amount = int (input_amount * LAMPORTS_PER_SOL )
56
78
else :
57
79
mint_pubkey = Pubkey .from_string (input_mint )
58
80
resp = await wallet .client .get_account_info (mint_pubkey )
@@ -65,11 +87,8 @@ async def trade(
65
87
raise ValueError (
66
88
f"Unsupported token program: { owner } . Supported programs are SPL Token and Token 2022."
67
89
)
68
-
69
- token = AsyncToken (
70
- wallet .client , mint_pubkey , program_id , wallet .fee_payer
71
- )
72
-
90
+ from spl .token .async_client import AsyncToken
91
+ token = AsyncToken (wallet .client , mint_pubkey , program_id , wallet .fee_payer )
73
92
mint_info = await token .get_mint_info ()
74
93
adjusted_amount = int (input_amount * (10 ** mint_info .decimals ))
75
94
@@ -94,8 +113,8 @@ async def trade(
94
113
)
95
114
quote_data = quote_response .json ()
96
115
97
- swap_response = await client .post (
98
- f"{ jupiter_url } /swap" ,
116
+ swap_instructions_response = await client .post (
117
+ f"{ jupiter_url } /swap-instructions " ,
99
118
json = {
100
119
"quoteResponse" : quote_data ,
101
120
"userPublicKey" : str (wallet .pubkey ),
@@ -104,31 +123,148 @@ async def trade(
104
123
"prioritizationFeeLamports" : "auto" ,
105
124
},
106
125
)
107
- if swap_response .status_code != 200 :
126
+ if swap_instructions_response .status_code != 200 :
108
127
raise Exception (
109
- f"Failed to fetch swap transaction : { swap_response .status_code } "
128
+ f"Failed to fetch swap instructions : { swap_instructions_response .status_code } "
110
129
)
111
- swap_data = swap_response .json ()
130
+ swap_instructions_data = swap_instructions_response .json ()
112
131
113
- swap_transaction_buf = base64 . b64decode ( swap_data [ "swapTransaction" ])
114
- transaction = VersionedTransaction . from_bytes ( swap_transaction_buf )
132
+ # Build all instructions in order
133
+ instructions = []
115
134
116
- if no_signer :
117
- signature = NullSigner (wallet .pubkey ).sign_message (
118
- to_bytes_versioned (transaction .message )
119
- )
120
- signed_transaction = VersionedTransaction .populate (
121
- transaction .message , [signature ]
135
+ # 1. Compute budget instructions (if present)
136
+ has_cu = (
137
+ "computeBudgetInstructions" in swap_instructions_data
138
+ and swap_instructions_data ["computeBudgetInstructions" ]
139
+ )
140
+ if has_cu :
141
+ instructions += TradeManager .parse_instruction_list (swap_instructions_data ["computeBudgetInstructions" ])
142
+
143
+ # 2. Setup instructions (if present)
144
+ if "setupInstructions" in swap_instructions_data and swap_instructions_data ["setupInstructions" ]:
145
+ instructions += TradeManager .parse_instruction_list (swap_instructions_data ["setupInstructions" ])
146
+
147
+ # 3. Fee instruction (if wallet.fee_payer)
148
+ if wallet .fee_payer :
149
+ if input_mint == "So11111111111111111111111111111111111111112" :
150
+ ix_fee = transfer (
151
+ TransferParams (
152
+ from_pubkey = wallet .pubkey ,
153
+ to_pubkey = wallet .fee_payer .pubkey (),
154
+ lamports = int (input_amount * LAMPORTS_PER_SOL * (fee_percentage / 100 )),
155
+ )
156
+ )
157
+ else :
158
+ mint_pubkey = Pubkey .from_string (input_mint )
159
+ resp = await wallet .client .get_account_info (mint_pubkey )
160
+ owner = str (resp .value .owner )
161
+ if owner == SPL_TOKEN_PROGRAM_ID :
162
+ program_id = Pubkey .from_string (SPL_TOKEN_PROGRAM_ID )
163
+ elif owner == TOKEN_2022_PROGRAM_ID :
164
+ program_id = Pubkey .from_string (TOKEN_2022_PROGRAM_ID )
165
+ else :
166
+ raise ValueError (
167
+ f"Unsupported token program: { owner } . Supported programs are SPL Token and Token 2022."
168
+ )
169
+
170
+ token = AsyncToken (
171
+ wallet .client , mint_pubkey , program_id , wallet .fee_payer
172
+ )
173
+
174
+ from_ata = (
175
+ (await token .get_accounts_by_owner (wallet .pubkey )).value [0 ].pubkey
176
+ )
177
+ mint_info = await token .get_mint_info ()
178
+ adjusted_amount = int (input_amount * (10 ** mint_info .decimals ))
179
+
180
+ to_fee_ata = (await token .get_accounts_by_owner (wallet .fee_payer .pubkey ())).value [0 ].pubkey
181
+ fee_amount = int (adjusted_amount * (fee_percentage / 100 ))
182
+ ix_fee = spl_transfer (
183
+ SPLTransferParams (
184
+ program_id = program_id ,
185
+ source = from_ata ,
186
+ mint = mint_pubkey ,
187
+ dest = to_fee_ata ,
188
+ owner = wallet .pubkey ,
189
+ amount = fee_amount ,
190
+ decimals = mint_info .decimals ,
191
+ )
192
+ )
193
+ instructions .append (ix_fee )
194
+
195
+ # 4. Swap instruction (required)
196
+ swap_instruction = TradeManager .parse_instruction (swap_instructions_data ["swapInstruction" ])
197
+ instructions .append (swap_instruction )
198
+
199
+ # 5. Cleanup instruction (if present)
200
+ if "cleanupInstruction" in swap_instructions_data and swap_instructions_data ["cleanupInstruction" ]:
201
+ instructions .append (TradeManager .parse_instruction (swap_instructions_data ["cleanupInstruction" ]))
202
+
203
+ # 6. Other instructions (if present)
204
+ if "otherInstructions" in swap_instructions_data and swap_instructions_data ["otherInstructions" ]:
205
+ instructions += TradeManager .parse_instruction_list (swap_instructions_data ["otherInstructions" ])
206
+
207
+ # Address lookup tables
208
+ address_table_lookups = TradeManager .parse_address_table_lookups (
209
+ swap_instructions_data .get ("addressLookupTableAddresses" , [])
210
+ )
211
+
212
+ # Simulate to estimate compute units (only if Jupiter did NOT provide CU instructions)
213
+ if not has_cu :
214
+ blockhash_response = await wallet .client .get_latest_blockhash (commitment = Finalized )
215
+ recent_blockhash = blockhash_response .value .blockhash
216
+
217
+ msg = MessageV0 .try_compile (
218
+ wallet .pubkey ,
219
+ instructions ,
220
+ [],
221
+ recent_blockhash ,
122
222
)
123
- return signed_transaction
223
+ transaction = VersionedTransaction .populate (msg , [Signature .default ()])
224
+ cu_units = (
225
+ await wallet .client .simulate_transaction (
226
+ transaction , sig_verify = False
227
+ )
228
+ ).value .units_consumed or 1_400_000
124
229
125
- signature = wallet .sign_message (to_bytes_versioned (transaction .message ))
126
- signed_transaction = VersionedTransaction .populate (
127
- transaction .message , [signature ]
230
+ compute_budget_ix = set_compute_unit_limit (int (cu_units + 100_000 ))
231
+
232
+ # Priority fee (helius)
233
+ priority_fee_ix = None
234
+ if provider == "helius" :
235
+ priority_fee = swap_instructions_data .get ("prioritizationFeeLamports" , 0 )
236
+ if priority_fee and priority_fee > 0 :
237
+ priority_fee_ix = set_compute_unit_price (priority_fee )
238
+
239
+ # Insert compute budget and priority fee at the start
240
+ instructions_with_cu = [compute_budget_ix ]
241
+ if priority_fee_ix :
242
+ instructions_with_cu .append (priority_fee_ix )
243
+ instructions_with_cu += instructions
244
+ else :
245
+ instructions_with_cu = instructions
246
+
247
+ # Re-compile with compute budget and priority fee, now with address_table_lookups
248
+ blockhash_response = await wallet .client .get_latest_blockhash (commitment = Finalized )
249
+ recent_blockhash = blockhash_response .value .blockhash
250
+
251
+ msg_final = MessageV0 .try_compile (
252
+ wallet .pubkey ,
253
+ instructions_with_cu ,
254
+ address_table_lookups ,
255
+ recent_blockhash ,
128
256
)
129
257
258
+ # Sign and return the transaction
259
+ if no_signer :
260
+ signature = NullSigner (wallet .pubkey ).sign_message (to_bytes_versioned (msg_final ))
261
+ signed_transaction = VersionedTransaction .populate (msg_final , [signature ])
262
+ return signed_transaction
263
+
264
+ signature = wallet .sign_message (to_bytes_versioned (msg_final ))
265
+ signed_transaction = VersionedTransaction .populate (msg_final , [signature ])
130
266
return signed_transaction
131
267
132
268
except Exception as e :
133
269
logging .exception (f"Swap failed: { str (e )} " )
134
- raise Exception (f"Swap failed: { str (e )} " )
270
+ raise Exception (f"Swap failed: { str (e )} " )
0 commit comments