Skip to content

Commit a0c201e

Browse files
v15.1.4 (#55)
1 parent 142d44e commit a0c201e

File tree

10 files changed

+150
-6
lines changed

10 files changed

+150
-6
lines changed

.coverage

0 Bytes
Binary file not shown.

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 = "15.1.3"
3+
version = "15.1.4"
44
description = "Solana Agent Kit"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"

sakit/privy_dflow_prediction.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232

3333
from sakit.utils.dflow import DFlowPredictionClient
3434
from sakit.utils.trigger import replace_blockhash_in_transaction, get_fresh_blockhash
35-
from sakit.utils.wallet import send_raw_transaction_with_priority
35+
from sakit.utils.wallet import (
36+
send_raw_transaction_with_priority,
37+
sanitize_privy_user_id,
38+
)
3639

3740
logger = logging.getLogger(__name__)
3841

@@ -534,6 +537,9 @@ async def execute(
534537
) -> Dict[str, Any]:
535538
"""Execute a prediction market action using Privy embedded wallet."""
536539

540+
# Sanitize privy_user_id to handle LLM formatting errors
541+
privy_user_id = sanitize_privy_user_id(privy_user_id)
542+
537543
client = self._get_client(include_risky)
538544

539545
try:

sakit/privy_dflow_swap.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
from solders.message import to_bytes_versioned # type: ignore
2727

2828
from sakit.utils.dflow import DFlowSwap
29-
from sakit.utils.wallet import send_raw_transaction_with_priority
29+
from sakit.utils.wallet import (
30+
send_raw_transaction_with_priority,
31+
sanitize_privy_user_id,
32+
)
3033

3134
logger = logging.getLogger(__name__)
3235

@@ -260,6 +263,9 @@ async def execute( # pragma: no cover
260263
amount: str,
261264
slippage_bps: int = 0,
262265
) -> Dict[str, Any]:
266+
# Sanitize user_id to handle LLM formatting errors
267+
user_id = sanitize_privy_user_id(user_id) or user_id
268+
263269
if not all([self._app_id, self._app_secret, self._signing_key]):
264270
return {"status": "error", "message": "Privy config missing."}
265271

sakit/privy_recurring.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from solders.message import to_bytes_versioned # type: ignore
1919

2020
from sakit.utils.recurring import JupiterRecurring
21+
from sakit.utils.wallet import sanitize_privy_user_id
2122

2223
logger = logging.getLogger(__name__)
2324

@@ -292,6 +293,9 @@ async def execute(
292293
start_at: Optional[str] = None,
293294
order_pubkey: Optional[str] = None,
294295
) -> Dict[str, Any]:
296+
# Sanitize user_id to handle LLM formatting errors
297+
user_id = sanitize_privy_user_id(user_id) or user_id
298+
295299
if not all([self._app_id, self._app_secret, self._signing_key]):
296300
return {"status": "error", "message": "Privy config missing."}
297301

sakit/privy_transfer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from privy import AsyncPrivyAPI
66
from privy.lib.authorization_signatures import get_authorization_signature
77
from cryptography.hazmat.primitives import serialization
8-
from sakit.utils.wallet import SolanaWalletClient
8+
from sakit.utils.wallet import SolanaWalletClient, sanitize_privy_user_id
99
from sakit.utils.transfer import TokenTransferManager
1010

1111
logger = logging.getLogger(__name__)
@@ -226,6 +226,9 @@ async def execute(
226226
mint: str,
227227
memo: str = "",
228228
) -> Dict[str, Any]:
229+
# Sanitize user_id to handle LLM formatting errors
230+
user_id = sanitize_privy_user_id(user_id) or user_id
231+
229232
if not all(
230233
[
231234
self.app_id,

sakit/privy_trigger.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
replace_blockhash_in_transaction,
2525
get_fresh_blockhash,
2626
)
27-
from sakit.utils.wallet import send_raw_transaction_with_priority
27+
from sakit.utils.wallet import (
28+
send_raw_transaction_with_priority,
29+
sanitize_privy_user_id,
30+
)
2831

2932
logger = logging.getLogger(__name__)
3033

@@ -319,6 +322,9 @@ async def execute(
319322
expired_at: Optional[str] = None,
320323
order_pubkey: Optional[str] = None,
321324
) -> Dict[str, Any]:
325+
# Sanitize user_id to handle LLM formatting errors
326+
user_id = sanitize_privy_user_id(user_id) or user_id
327+
322328
if not all([self._app_id, self._app_secret, self._signing_key]):
323329
return {"status": "error", "message": "Privy config missing."}
324330

sakit/privy_ultra.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323

2424
from sakit.utils.ultra import JupiterUltra
2525
from sakit.utils.trigger import replace_blockhash_in_transaction, get_fresh_blockhash
26-
from sakit.utils.wallet import send_raw_transaction_with_priority
26+
from sakit.utils.wallet import (
27+
send_raw_transaction_with_priority,
28+
sanitize_privy_user_id,
29+
)
2730

2831
logger = logging.getLogger(__name__)
2932

@@ -467,6 +470,9 @@ async def execute( # pragma: no cover
467470
output_mint: str,
468471
amount: int,
469472
) -> Dict[str, Any]:
473+
# Sanitize user_id to handle LLM formatting errors
474+
user_id = sanitize_privy_user_id(user_id) or user_id
475+
470476
if not all([self.app_id, self.app_secret, self.signing_key]):
471477
return {"status": "error", "message": "Privy config missing."}
472478

sakit/utils/wallet.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@
1515
logger = logging.getLogger(__name__)
1616

1717

18+
def sanitize_privy_user_id(user_id: Optional[str]) -> Optional[str]:
19+
"""
20+
Sanitize and normalize privy user_id to handle LLM formatting errors.
21+
22+
LLMs sometimes mangle the user ID by:
23+
- Adding quotes: "did:privy:xxx" or 'did:privy:xxx'
24+
- Changing case: Did:privy:xxx or DID:PRIVY:xxx
25+
- Adding whitespace
26+
27+
This function normalizes to the correct format: did:privy:xxx
28+
29+
Args:
30+
user_id: The raw user_id string from LLM
31+
32+
Returns:
33+
Sanitized user_id or None if input was None/empty
34+
"""
35+
if not user_id:
36+
return None
37+
38+
# Strip whitespace and quotes
39+
cleaned = user_id.strip().strip('"').strip("'").strip()
40+
41+
if not cleaned:
42+
return None
43+
44+
# Normalize the did:privy: prefix to lowercase
45+
if cleaned.lower().startswith("did:privy:"):
46+
# Keep the unique ID part as-is, but normalize prefix
47+
unique_part = cleaned[10:] # Everything after "did:privy:"
48+
return f"did:privy:{unique_part}"
49+
50+
return cleaned
51+
52+
1853
class SolanaTransaction:
1954
"""Transaction parameters for Solana."""
2055

tests/test_privy_dflow_prediction_tool.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
PrivyDFlowPredictionTool,
1414
PrivyDFlowPredictionPlugin,
1515
)
16+
from sakit.utils.wallet import sanitize_privy_user_id
1617
from sakit.utils.dflow import (
1718
DFlowPredictionClient,
1819
DFlowPredictionOrderResult,
@@ -101,6 +102,83 @@ def sample_event():
101102
}
102103

103104

105+
# =============================================================================
106+
# PRIVY USER ID SANITIZATION TESTS
107+
# =============================================================================
108+
109+
110+
class TestPrivyUserIdSanitization:
111+
"""Test privy_user_id sanitization to handle LLM formatting errors."""
112+
113+
def test_sanitize_none(self):
114+
"""None should return None."""
115+
assert sanitize_privy_user_id(None) is None
116+
117+
def test_sanitize_empty_string(self):
118+
"""Empty string should return None."""
119+
assert sanitize_privy_user_id("") is None
120+
121+
def test_sanitize_correct_format(self):
122+
"""Correctly formatted ID should be returned as-is."""
123+
user_id = "did:privy:cmiox5jks01fhk40d0feu1wt8"
124+
assert sanitize_privy_user_id(user_id) == user_id
125+
126+
def test_sanitize_with_leading_quote(self):
127+
"""Leading quote should be stripped."""
128+
user_id = '"did:privy:cmiox5jks01fhk40d0feu1wt8'
129+
expected = "did:privy:cmiox5jks01fhk40d0feu1wt8"
130+
assert sanitize_privy_user_id(user_id) == expected
131+
132+
def test_sanitize_with_capitalized_did(self):
133+
"""Capitalized Did should be normalized."""
134+
user_id = "Did:privy:cmiox5jks01fhk40d0feu1wt8"
135+
expected = "did:privy:cmiox5jks01fhk40d0feu1wt8"
136+
assert sanitize_privy_user_id(user_id) == expected
137+
138+
def test_sanitize_with_all_caps(self):
139+
"""All caps DID:PRIVY should be normalized."""
140+
user_id = "DID:PRIVY:cmiox5jks01fhk40d0feu1wt8"
141+
expected = "did:privy:cmiox5jks01fhk40d0feu1wt8"
142+
assert sanitize_privy_user_id(user_id) == expected
143+
144+
def test_sanitize_with_quotes_and_caps(self):
145+
"""Leading quote and caps should both be fixed."""
146+
user_id = '"Did:privy:cmiox5jks01fhk40d0feu1wt8'
147+
expected = "did:privy:cmiox5jks01fhk40d0feu1wt8"
148+
assert sanitize_privy_user_id(user_id) == expected
149+
150+
def test_sanitize_with_single_quotes(self):
151+
"""Single quotes should be stripped."""
152+
user_id = "'did:privy:cmiox5jks01fhk40d0feu1wt8'"
153+
expected = "did:privy:cmiox5jks01fhk40d0feu1wt8"
154+
assert sanitize_privy_user_id(user_id) == expected
155+
156+
def test_sanitize_with_whitespace(self):
157+
"""Whitespace should be stripped."""
158+
user_id = " did:privy:cmiox5jks01fhk40d0feu1wt8 "
159+
expected = "did:privy:cmiox5jks01fhk40d0feu1wt8"
160+
assert sanitize_privy_user_id(user_id) == expected
161+
162+
def test_sanitize_preserves_unique_id_case(self):
163+
"""Unique ID part should preserve its original case."""
164+
user_id = "did:privy:CmIoX5jKs01fHk40D0fEu1wT8"
165+
expected = "did:privy:CmIoX5jKs01fHk40D0fEu1wT8"
166+
assert sanitize_privy_user_id(user_id) == expected
167+
168+
def test_sanitize_non_privy_id(self):
169+
"""Non-privy ID should be returned cleaned but not normalized."""
170+
user_id = " some-other-id "
171+
expected = "some-other-id"
172+
assert sanitize_privy_user_id(user_id) == expected
173+
174+
def test_sanitize_only_whitespace_and_quotes(self):
175+
"""String with only whitespace and quotes should return None."""
176+
assert sanitize_privy_user_id(" ") is None
177+
assert sanitize_privy_user_id('""') is None
178+
assert sanitize_privy_user_id("''") is None
179+
assert sanitize_privy_user_id('" "') is None
180+
181+
104182
# =============================================================================
105183
# TOOL INITIALIZATION TESTS
106184
# =============================================================================

0 commit comments

Comments
 (0)