Skip to content

Commit a5f44c7

Browse files
v15.2.0 (#56)
1 parent a0c201e commit a5f44c7

File tree

7 files changed

+671
-50
lines changed

7 files changed

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

sakit/dflow_prediction.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -537,26 +537,25 @@ async def sign_and_send(tx_b64: str) -> str:
537537

538538
elif action == "positions":
539539
# For positions, we need to check the user's token balances
540-
# This requires querying on-chain data for outcome token holdings
541540
if not self._private_key:
542541
return {"status": "error", "message": "private_key not configured"}
543542

543+
if not self._rpc_url:
544+
return {
545+
"status": "error",
546+
"message": "rpc_url must be configured to query positions",
547+
}
548+
544549
keypair = Keypair.from_base58_string(self._private_key)
545550
user_pubkey = str(keypair.pubkey())
546551

547-
# Get all outcome mints to identify which tokens are prediction outcomes
548-
outcome_mints = await client.get_outcome_mints()
549-
550-
return {
551-
"status": "success",
552-
"message": "Position tracking requires on-chain token balance queries. "
553-
"Use a wallet tool to check balances for outcome token mints.",
554-
"user_wallet": user_pubkey,
555-
"outcome_mint_count": len(outcome_mints)
556-
if isinstance(outcome_mints, list)
557-
else "see data",
558-
"hint": "Query token balances for mints from outcome_mints to find positions.",
559-
}
552+
# Query positions via RPC + DFlow outcome mints
553+
positions_result = await client.get_positions(
554+
wallet_address=user_pubkey,
555+
rpc_url=self._rpc_url,
556+
)
557+
558+
return positions_result
560559

561560
else:
562561
return {"status": "error", "message": f"Unknown action: {action}"}

sakit/privy_dflow_prediction.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -832,13 +832,19 @@ async def sign_and_send(tx_b64: str) -> str: # pragma: no cover
832832
}
833833

834834
elif action == "positions":
835-
# For positions, we need the user's wallet address
835+
# For positions, we need the user's wallet address and RPC
836836
if not privy_user_id:
837837
return {
838838
"status": "error",
839839
"message": "privy_user_id is required for positions action",
840840
}
841841

842+
if not self._rpc_url:
843+
return {
844+
"status": "error",
845+
"message": "rpc_url must be configured to query positions",
846+
}
847+
842848
# Validate Privy configuration
843849
if not self._privy_app_id or not self._privy_app_secret:
844850
return {
@@ -857,19 +863,13 @@ async def sign_and_send(tx_b64: str) -> str: # pragma: no cover
857863

858864
wallet_address = wallet["public_key"]
859865

860-
# Get all outcome mints to identify which tokens are prediction outcomes
861-
outcome_mints = await client.get_outcome_mints()
862-
863-
return {
864-
"status": "success",
865-
"message": "Position tracking requires on-chain token balance queries. "
866-
"Use a wallet tool to check balances for outcome token mints.",
867-
"user_wallet": wallet_address,
868-
"outcome_mint_count": len(outcome_mints)
869-
if isinstance(outcome_mints, list)
870-
else "see data",
871-
"hint": "Query token balances for mints from outcome_mints to find positions.",
872-
}
866+
# Query positions via RPC + DFlow outcome mints
867+
positions_result = await client.get_positions(
868+
wallet_address=wallet_address,
869+
rpc_url=self._rpc_url,
870+
)
871+
872+
return positions_result
873873

874874
else:
875875
return {"status": "error", "message": f"Unknown action: {action}"}

sakit/utils/dflow.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,3 +1169,127 @@ async def execute_prediction_order_blocking(
11691169
price_impact_pct=None,
11701170
error=str(e),
11711171
)
1172+
1173+
async def get_positions(
1174+
self,
1175+
wallet_address: str,
1176+
rpc_url: str,
1177+
) -> Dict[str, Any]:
1178+
"""
1179+
Get prediction market positions for a wallet.
1180+
1181+
Queries token accounts via RPC and cross-references with DFlow outcome mints
1182+
to identify prediction market positions.
1183+
1184+
Args:
1185+
wallet_address: Solana wallet address to check
1186+
rpc_url: RPC endpoint URL
1187+
1188+
Returns:
1189+
Dict with positions list and summary
1190+
"""
1191+
# Get all outcome mints from DFlow
1192+
outcome_mints = await self.get_outcome_mints()
1193+
1194+
# Query wallet token accounts via RPC
1195+
token_accounts = await _get_token_accounts(rpc_url, wallet_address)
1196+
1197+
if "error" in token_accounts:
1198+
return {
1199+
"status": "error",
1200+
"message": f"Failed to query token accounts: {token_accounts['error']}",
1201+
}
1202+
1203+
# Cross-reference to find prediction positions
1204+
positions = []
1205+
1206+
for account in token_accounts.get("accounts", []):
1207+
mint = account.get("mint")
1208+
if mint and mint in outcome_mints:
1209+
market_info = outcome_mints[mint]
1210+
amount = account.get("amount", "0")
1211+
ui_amount = account.get("uiAmount", 0)
1212+
1213+
# Only include if they have a balance
1214+
if ui_amount > 0:
1215+
positions.append(
1216+
{
1217+
"mint": mint,
1218+
"ticker": market_info.get(
1219+
"market", market_info.get("ticker", "unknown")
1220+
),
1221+
"side": market_info.get("side", "unknown").upper(),
1222+
"amount": amount,
1223+
"ui_amount": ui_amount,
1224+
"decimals": account.get("decimals", 6),
1225+
}
1226+
)
1227+
1228+
return {
1229+
"status": "success",
1230+
"wallet": wallet_address,
1231+
"position_count": len(positions),
1232+
"positions": positions,
1233+
"hint": "Each position represents outcome tokens. Sell to exit or hold until resolution."
1234+
if positions
1235+
else "No prediction market positions found.",
1236+
}
1237+
1238+
1239+
async def _get_token_accounts(rpc_url: str, wallet_address: str) -> Dict[str, Any]:
1240+
"""
1241+
Get all SPL token accounts for a wallet via RPC.
1242+
1243+
Args:
1244+
rpc_url: RPC endpoint URL
1245+
wallet_address: Wallet public key
1246+
1247+
Returns:
1248+
Dict with 'accounts' list or 'error' on failure
1249+
"""
1250+
# Token Program ID
1251+
TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
1252+
1253+
payload = {
1254+
"jsonrpc": "2.0",
1255+
"id": 1,
1256+
"method": "getTokenAccountsByOwner",
1257+
"params": [
1258+
wallet_address,
1259+
{"programId": TOKEN_PROGRAM_ID},
1260+
{"encoding": "jsonParsed"},
1261+
],
1262+
}
1263+
1264+
try:
1265+
async with httpx.AsyncClient(timeout=30.0) as client:
1266+
response = await client.post(rpc_url, json=payload)
1267+
if response.status_code != 200:
1268+
return {"error": f"RPC error: {response.status_code}"}
1269+
1270+
data = response.json()
1271+
if "error" in data:
1272+
return {"error": f"RPC error: {data['error']}"}
1273+
1274+
result = data.get("result", {}).get("value", [])
1275+
1276+
accounts = []
1277+
for item in result:
1278+
parsed = item.get("account", {}).get("data", {}).get("parsed", {})
1279+
info = parsed.get("info", {})
1280+
token_amount = info.get("tokenAmount", {})
1281+
1282+
accounts.append(
1283+
{
1284+
"mint": info.get("mint"),
1285+
"amount": token_amount.get("amount", "0"),
1286+
"uiAmount": float(token_amount.get("uiAmount", 0) or 0),
1287+
"decimals": token_amount.get("decimals", 0),
1288+
}
1289+
)
1290+
1291+
return {"accounts": accounts}
1292+
1293+
except Exception as e:
1294+
logger.exception("Failed to get token accounts")
1295+
return {"error": str(e)}

0 commit comments

Comments
 (0)