Skip to content

Commit f1be402

Browse files
v14.1.2 (#33)
1 parent 45def06 commit f1be402

File tree

5 files changed

+159
-21
lines changed

5 files changed

+159
-21
lines changed

.coverage

-520 KB
Binary file not shown.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,13 +365,16 @@ config = {
365365

366366
This plugin enables Solana Agent to swap tokens using DFlow's Swap API with Privy delegated wallets. DFlow offers faster swaps compared to Jupiter Ultra with competitive rates and supports platform fees for monetization.
367367

368+
Transactions are signed via Privy and sent via your configured RPC (Helius recommended) for reliable blockhash handling and priority fees.
369+
368370
```python
369371
config = {
370372
"tools": {
371373
"privy_dflow_swap": {
372374
"app_id": "your-privy-app-id", # Required - your Privy application ID
373375
"app_secret": "your-privy-app-secret", # Required - your Privy application secret
374376
"signing_key": "wallet-auth:your-signing-key", # Required - your Privy wallet authorization signing key
377+
"rpc_url": "https://mainnet.helius-rpc.com/?api-key=YOUR_KEY", # Required - Helius recommended for priority fees
375378
"platform_fee_bps": 50, # Optional - platform fee in basis points (e.g., 50 = 0.5%)
376379
"fee_account": "your-fee-token-account", # Optional - token account to receive platform fees
377380
"payer_private_key": "payer-private-key", # Optional - for gasless/sponsored transactions
@@ -384,8 +387,12 @@ config = {
384387
- **Fast Swaps**: DFlow typically executes faster than Jupiter Ultra
385388
- **Platform Fees**: Collect fees on swaps (in basis points) paid to your fee account
386389
- **Privy Delegated Wallets**: Seamless user experience with embedded wallets
390+
- **Helius Priority Fees**: Uses Helius priority fee estimation for reliable transaction landing
387391
- **Gasless Transactions**: Optionally sponsor gas fees for users via `payer_private_key`
388392

393+
**RPC URL (Required):**
394+
Helius RPC is strongly recommended (`https://mainnet.helius-rpc.com/?api-key=YOUR_KEY`). Helius provides priority fee estimation and better blockhash handling, which significantly improves transaction success rates. Get a free API key at [helius.dev](https://helius.dev).
395+
389396
**Platform Fee Setup:**
390397
To collect platform fees, you need to create a token account for the output token and provide it as `fee_account`. The `platform_fee_bps` specifies the fee amount (e.g., 50 = 0.5%).
391398

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sakit"
3-
version = "14.1.1"
3+
version = "14.1.2"
44
description = "Solana Agent Kit"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"

sakit/privy_dflow_swap.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
77
DFlow offers faster swaps compared to Jupiter Ultra with similar liquidity
88
and supports platform fees for monetization.
9+
10+
Transactions are signed via Privy's signTransaction and sent via Helius RPC
11+
for priority fees and reliable blockhash handling.
912
"""
1013

1114
import base64
@@ -21,6 +24,7 @@
2124
from solders.message import to_bytes_versioned # type: ignore
2225

2326
from sakit.utils.dflow import DFlowSwap
27+
from sakit.utils.wallet import send_raw_transaction_with_priority
2428

2529
logger = logging.getLogger(__name__)
2630

@@ -127,27 +131,27 @@ async def _get_privy_embedded_wallet( # pragma: no cover
127131
return None
128132

129133

130-
async def _privy_sign_and_send_transaction( # pragma: no cover
134+
async def _privy_sign_transaction( # pragma: no cover
131135
privy_client: AsyncPrivyAPI,
132136
wallet_id: str,
133137
encoded_tx: str,
134138
signing_key: str,
135139
) -> Dict[str, Any]:
136-
"""Sign and send a Solana transaction via Privy using the official SDK.
140+
"""Sign a Solana transaction via Privy using the official SDK.
137141
138-
Uses Privy's signAndSendTransaction RPC method which handles both
139-
signing with the delegated wallet and broadcasting to Solana in one call.
142+
Uses Privy's signTransaction RPC method to sign the transaction without
143+
broadcasting. The signed transaction should then be sent via Helius or
144+
another RPC provider for reliable blockhash handling.
140145
141146
Returns:
142-
Dict with 'hash' (transaction signature) on success, or error details.
147+
Dict with 'signed_transaction' (base64 encoded) on success, or error details.
143148
"""
144149
try:
145150
pkcs8_key = _convert_key_to_pkcs8_pem(signing_key)
146151

147-
# IMPORTANT: signAndSendTransaction requires caip2 for Solana mainnet
148152
url = f"https://api.privy.io/v1/wallets/{wallet_id}/rpc"
149153
body = {
150-
"method": "signAndSendTransaction",
154+
"method": "signTransaction",
151155
"params": {"transaction": encoded_tx, "encoding": "base64"},
152156
"caip2": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
153157
"chain_type": "solana",
@@ -163,25 +167,32 @@ async def _privy_sign_and_send_transaction( # pragma: no cover
163167

164168
result = await privy_client.wallets.rpc(
165169
wallet_id=wallet_id,
166-
method="signAndSendTransaction",
170+
method="signTransaction",
167171
params={"transaction": encoded_tx, "encoding": "base64"},
168172
caip2="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
169173
chain_type="solana",
170174
privy_authorization_signature=auth_signature,
171175
)
172176

173-
# Extract the transaction signature (hash) from the result
177+
# Extract the signed transaction from the result
174178
if result.data:
175-
return {
176-
"success": True,
177-
"hash": getattr(result.data, "hash", None),
178-
}
179-
return {"success": False, "error": "No data returned from Privy"}
179+
signed_tx = getattr(result.data, "signed_transaction", None)
180+
if signed_tx:
181+
return {"success": True, "signed_transaction": signed_tx}
182+
# Some SDK versions return the transaction directly
183+
tx = getattr(result.data, "transaction", None)
184+
if tx:
185+
return {"success": True, "signed_transaction": tx}
186+
return {"success": False, "error": "No signed transaction returned from Privy"}
180187
except Exception as e:
181-
logger.error(f"Privy API error signing and sending transaction: {e}")
188+
logger.error(f"Privy API error signing transaction: {e}")
182189
return {"success": False, "error": str(e)}
183190

184191

192+
# Default RPC URL if none configured
193+
DEFAULT_RPC_URL = "https://api.mainnet-beta.solana.com"
194+
195+
185196
class PrivyDFlowSwapTool(AutoTool):
186197
"""Fast token swaps using DFlow API with Privy embedded wallets."""
187198

@@ -202,6 +213,7 @@ def __init__(self, registry: Optional[ToolRegistry] = None):
202213
self._fee_account: Optional[str] = None
203214
self._referral_account: Optional[str] = None
204215
self._payer_private_key: Optional[str] = None
216+
self._rpc_url: Optional[str] = None
205217

206218
def get_schema(self) -> Dict[str, Any]:
207219
return {
@@ -243,6 +255,8 @@ def configure(self, config: Dict[str, Any]) -> None:
243255
self._fee_account = tool_cfg.get("fee_account")
244256
self._referral_account = tool_cfg.get("referral_account")
245257
self._payer_private_key = tool_cfg.get("payer_private_key")
258+
# RPC URL for sending transactions (Helius recommended for priority fees)
259+
self._rpc_url = tool_cfg.get("rpc_url")
246260

247261
async def execute( # pragma: no cover
248262
self,
@@ -330,20 +344,39 @@ async def execute( # pragma: no cover
330344
"utf-8"
331345
)
332346

333-
# Sign and send via Privy (uses signAndSendTransaction RPC method)
334-
send_result = await _privy_sign_and_send_transaction(
347+
# Sign via Privy (signTransaction only, not signAndSendTransaction)
348+
sign_result = await _privy_sign_transaction(
335349
privy_client,
336350
wallet_id,
337351
tx_to_sign,
338352
self._signing_key,
339353
)
340354

355+
if not sign_result.get("success"):
356+
error_msg = sign_result.get("error", "Failed to sign transaction")
357+
return {"status": "error", "message": error_msg}
358+
359+
signed_tx_base64 = sign_result.get("signed_transaction")
360+
if not signed_tx_base64:
361+
return {
362+
"status": "error",
363+
"message": "No signed transaction returned from Privy.",
364+
}
365+
366+
# Send via RPC (Helius recommended for priority fees)
367+
rpc_url = self._rpc_url or DEFAULT_RPC_URL
368+
tx_bytes = base64.b64decode(signed_tx_base64)
369+
send_result = await send_raw_transaction_with_priority(
370+
rpc_url=rpc_url,
371+
tx_bytes=tx_bytes,
372+
)
373+
341374
if send_result.get("success"):
342-
signature = send_result.get("hash")
375+
signature = send_result.get("signature")
343376
if not signature:
344377
return {
345378
"status": "error",
346-
"message": "No transaction signature returned from Privy.",
379+
"message": "No transaction signature returned.",
347380
}
348381

349382
return {
@@ -376,7 +409,7 @@ async def execute( # pragma: no cover
376409
return {
377410
"status": "error",
378411
"message": send_result.get(
379-
"error", "Failed to sign and send transaction via Privy."
412+
"error", "Failed to send transaction."
380413
),
381414
}
382415

sakit/utils/wallet.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Dict, List, Optional
22
import httpx
33
import nacl.signing
4+
import logging
45
from solana.rpc.async_api import AsyncClient
56
from solana.rpc.commitment import Confirmed
67
from solana.rpc.types import TxOpts
@@ -11,6 +12,8 @@
1112
from solders.signature import Signature
1213
from solders.pubkey import Pubkey
1314

15+
logger = logging.getLogger(__name__)
16+
1417

1518
class SolanaTransaction:
1619
"""Transaction parameters for Solana."""
@@ -110,3 +113,98 @@ async def get_priority_fee_estimate_helius( # pragma: no cover
110113
if "error" in result:
111114
raise RuntimeError(f"Fee estimation failed: {result['error']}")
112115
return int(result["result"]["priorityFeeEstimate"])
116+
117+
118+
async def send_raw_transaction_with_priority( # pragma: no cover
119+
rpc_url: str,
120+
tx_bytes: bytes,
121+
skip_preflight: bool = True,
122+
max_retries: int = 5,
123+
) -> Dict[str, any]:
124+
"""
125+
Send a raw transaction to Solana RPC, with Helius priority fee logging if applicable.
126+
127+
This is the standard way to send pre-signed transactions (e.g., from DFlow, Jupiter)
128+
through Helius or any Solana RPC endpoint.
129+
130+
Args:
131+
rpc_url: The RPC endpoint URL (Helius recommended for priority fees)
132+
tx_bytes: The serialized signed transaction bytes
133+
skip_preflight: Skip preflight simulation (default True for pre-signed txs)
134+
max_retries: Number of retries for the RPC call
135+
136+
Returns:
137+
Dict with 'success' and 'signature' on success, or 'error' on failure.
138+
"""
139+
try:
140+
client = AsyncClient(rpc_url)
141+
try:
142+
# Get priority fee estimate if using Helius (for logging)
143+
if "helius" in rpc_url.lower():
144+
try:
145+
import base64
146+
147+
tx_base64 = base64.b64encode(tx_bytes).decode("utf-8")
148+
async with httpx.AsyncClient() as http_client:
149+
payload = {
150+
"jsonrpc": "2.0",
151+
"id": "1",
152+
"method": "getPriorityFeeEstimate",
153+
"params": [
154+
{
155+
"transaction": tx_base64,
156+
"options": {"recommended": True},
157+
}
158+
],
159+
}
160+
response = await http_client.post(rpc_url, json=payload)
161+
if response.status_code == 200:
162+
result = response.json()
163+
if "result" in result:
164+
priority_fee = result["result"].get(
165+
"priorityFeeEstimate", 0
166+
)
167+
logger.info(
168+
f"Helius priority fee estimate: {priority_fee}"
169+
)
170+
except Exception as fee_error:
171+
logger.debug(f"Could not get priority fee estimate: {fee_error}")
172+
173+
# Send the transaction
174+
result = await client.send_raw_transaction(
175+
tx_bytes,
176+
opts=TxOpts(
177+
skip_preflight=skip_preflight,
178+
preflight_commitment=Confirmed,
179+
max_retries=max_retries,
180+
),
181+
)
182+
183+
signature = str(result.value)
184+
logger.info(f"Transaction sent: {signature}")
185+
186+
# Confirm the transaction
187+
try:
188+
confirmation = await client.confirm_transaction(
189+
result.value,
190+
commitment=Confirmed,
191+
sleep_seconds=0.5,
192+
last_valid_block_height=None,
193+
)
194+
if confirmation.value and confirmation.value[0].err:
195+
return {
196+
"success": False,
197+
"error": f"Transaction failed: {confirmation.value[0].err}",
198+
}
199+
except Exception as confirm_error:
200+
logger.debug(f"Could not confirm transaction: {confirm_error}")
201+
# Still return success since transaction was sent
202+
203+
return {"success": True, "signature": signature}
204+
205+
finally:
206+
await client.close()
207+
208+
except Exception as e:
209+
logger.error(f"RPC error sending transaction: {e}")
210+
return {"success": False, "error": str(e)}

0 commit comments

Comments
 (0)