Skip to content

Commit 7b76757

Browse files
v12.4.0
1 parent cec9177 commit 7b76757

File tree

7 files changed

+843
-670
lines changed

7 files changed

+843
-670
lines changed

poetry.lock

Lines changed: 598 additions & 602 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sakit"
3-
version = "12.3.0"
3+
version = "12.4.0"
44
description = "Solana Agent Kit"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"
@@ -17,10 +17,10 @@ packages = [{ include = "sakit" }]
1717
python = ">=3.12,<4.0"
1818
httpx = "^0.28.1"
1919
solana-agent = ">=30.0.0"
20-
boto3 = "^1.38.32"
21-
botocore = "^1.38.32"
20+
boto3 = "^1.38.36"
21+
botocore = "^1.38.36"
2222
nemo-agent = "5.0.3"
23-
fastmcp = "^2.7.1"
23+
fastmcp = "^2.8.1"
2424
solana = "^0.36.7"
2525
solders = "^0.26.0"
2626
pynacl = "^1.5.0"

sakit/privy_swap.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def __init__(self, registry: Optional[ToolRegistry] = None):
9595
self.rpc_url = None
9696
self.jupiter_url = None
9797
self.fee_payer = None
98+
self.fee_percentage = 0.85 # Default fee percentage (0.85% = 0.0085)
9899

99100
def get_schema(self) -> Dict[str, Any]:
100101
return {
@@ -126,6 +127,7 @@ def configure(self, config: Dict[str, Any]) -> None:
126127
self.rpc_url = tool_cfg.get("rpc_url")
127128
self.jupiter_url = tool_cfg.get("jupiter_url")
128129
self.fee_payer = tool_cfg.get("fee_payer")
130+
self.fee_percentage = tool_cfg.get("fee_percentage", 0.85) # Default 0.85% fee
129131

130132
async def execute(
131133
self,
@@ -158,6 +160,9 @@ async def execute(
158160
wallet = SolanaWalletClient(
159161
self.rpc_url, None, wallet_info["public_key"], self.fee_payer
160162
)
163+
provider = None
164+
if "helius" in self.rpc_url:
165+
provider = "helius"
161166
transaction = await TradeManager.trade(
162167
wallet,
163168
output_mint,
@@ -166,6 +171,8 @@ async def execute(
166171
slippage_bps,
167172
self.jupiter_url,
168173
True,
174+
provider,
175+
self.fee_percentage,
169176
)
170177
encoded_transaction = base64.b64encode(bytes(transaction)).decode("utf-8")
171178
result = await privy_sign_and_send(

sakit/privy_transfer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def __init__(self, registry: Optional[ToolRegistry] = None):
9898
self.signing_key = None
9999
self.rpc_url = None
100100
self.fee_payer = None
101+
self.fee_percentage = None
101102

102103
def get_schema(self) -> Dict[str, Any]:
103104
return {
@@ -128,6 +129,7 @@ def configure(self, config: Dict[str, Any]) -> None:
128129
self.signing_key = tool_cfg.get("signing_key")
129130
self.rpc_url = tool_cfg.get("rpc_url")
130131
self.fee_payer = tool_cfg.get("fee_payer")
132+
self.fee_percentage = tool_cfg.get("fee_percentage", 0.85) # Default 0.85% fee
131133

132134
async def execute(
133135
self,
@@ -163,7 +165,7 @@ async def execute(
163165
if "helius" in self.rpc_url:
164166
provider = "helius"
165167
transaction = await TokenTransferManager.transfer(
166-
wallet, to_address, amount, mint, provider, True
168+
wallet, to_address, amount, mint, provider, True, self.fee_percentage
167169
)
168170
encoded_transaction = base64.b64encode(bytes(transaction)).decode("utf-8")
169171
result = await privy_sign_and_send(

sakit/utils/swap.py

Lines changed: 187 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
11
import logging
22
import base64
3-
from typing import Optional
3+
from typing import Optional, List
44
import httpx
55

66
from solders.pubkey import Pubkey
7+
from solana.rpc.commitment import Finalized
78
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+
)
1017
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
1124
from sakit.utils.wallet import SolanaWalletClient
25+
from sakit.utils.transfer import transfer, TransferParams
1226

1327
JUP_API = "https://quote-api.jup.ag/v6"
1428
SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
1529
TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
16-
30+
LAMPORTS_PER_SOL = 10**9
1731

1832
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+
1960
@staticmethod
2061
async def trade(
2162
wallet: SolanaWalletClient,
@@ -25,34 +66,15 @@ async def trade(
2566
slippage_bps: int = 300,
2667
jupiter_url: Optional[str] = None,
2768
no_signer: bool = False,
69+
provider: Optional[str] = None,
70+
fee_percentage: float = 0.85,
2871
) -> VersionedTransaction:
2972
"""
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').
4674
"""
4775
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)
5678
else:
5779
mint_pubkey = Pubkey.from_string(input_mint)
5880
resp = await wallet.client.get_account_info(mint_pubkey)
@@ -65,11 +87,8 @@ async def trade(
6587
raise ValueError(
6688
f"Unsupported token program: {owner}. Supported programs are SPL Token and Token 2022."
6789
)
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)
7392
mint_info = await token.get_mint_info()
7493
adjusted_amount = int(input_amount * (10**mint_info.decimals))
7594

@@ -94,8 +113,8 @@ async def trade(
94113
)
95114
quote_data = quote_response.json()
96115

97-
swap_response = await client.post(
98-
f"{jupiter_url}/swap",
116+
swap_instructions_response = await client.post(
117+
f"{jupiter_url}/swap-instructions",
99118
json={
100119
"quoteResponse": quote_data,
101120
"userPublicKey": str(wallet.pubkey),
@@ -104,31 +123,148 @@ async def trade(
104123
"prioritizationFeeLamports": "auto",
105124
},
106125
)
107-
if swap_response.status_code != 200:
126+
if swap_instructions_response.status_code != 200:
108127
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}"
110129
)
111-
swap_data = swap_response.json()
130+
swap_instructions_data = swap_instructions_response.json()
112131

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 = []
115134

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,
122222
)
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
124229

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,
128256
)
129257

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])
130266
return signed_transaction
131267

132268
except Exception as e:
133269
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

Comments
 (0)