Skip to content

Commit ecf7462

Browse files
v14.3.1 (#48)
1 parent e1e7253 commit ecf7462

File tree

5 files changed

+117
-32
lines changed

5 files changed

+117
-32
lines changed

.coverage

4 KB
Binary file not shown.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,9 @@ config = {
890890
- `price_change_percentage`: Use "-0.5" for 0.5% lower (buy the dip), "10" for 10% higher (sell high)
891891

892892
- `limit_order_info` - Calculate trigger price and USD values for displaying order info
893-
- Params: `making_amount`, `taking_amount`, `input_price_usd`, `output_price_usd`
893+
- Params: `making_amount`, `taking_amount`, `input_price_usd`, `output_price_usd`, `input_decimals`, `output_decimals`
894+
- `making_amount` and `taking_amount` should be raw amounts from order's `rawMakingAmount` and `rawTakingAmount` fields
895+
- `input_decimals` and `output_decimals` are REQUIRED (e.g., SOL=9, USDC=6, BONK=5)
894896
- Returns: `making_usd`, `taking_usd_at_current`, `trigger_price_usd`, `current_output_price_usd`, `price_difference_percent`, `should_fill_now`
895897
- Use this when listing orders to show meaningful price info to users
896898

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

sakit/token_math.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,27 +218,42 @@ def calculate_limit_order_info(
218218
taking_amount: str,
219219
input_price_usd: str,
220220
output_price_usd: str,
221+
input_decimals: int = 0,
222+
output_decimals: int = 0,
221223
) -> Dict[str, str]:
222224
"""
223225
Calculate display info for a limit order from its amounts.
224226
225227
Use this when listing orders to show USD values and trigger prices.
226228
227229
Args:
228-
making_amount: Human-readable amount of input token being sold
229-
taking_amount: Human-readable amount of output token to receive
230+
making_amount: Raw amount of input token in smallest units (e.g., lamports)
231+
taking_amount: Raw amount of output token in smallest units
230232
input_price_usd: Current market price of input token in USD
231233
output_price_usd: Current market price of output token in USD
234+
input_decimals: Decimals for input token (e.g., 9 for SOL). If 0, assumes human-readable.
235+
output_decimals: Decimals for output token (e.g., 5 for BONK). If 0, assumes human-readable.
232236
233237
Returns:
234238
Dict with USD values and trigger price info
235239
"""
236240
try:
237-
making = Decimal(str(making_amount))
238-
taking = Decimal(str(taking_amount))
241+
raw_making = Decimal(str(making_amount))
242+
raw_taking = Decimal(str(taking_amount))
239243
input_price = Decimal(str(input_price_usd))
240244
output_price = Decimal(str(output_price_usd))
241245

246+
# Convert from smallest units to human-readable if decimals provided
247+
if input_decimals > 0:
248+
making = raw_making / (Decimal(10) ** input_decimals)
249+
else:
250+
making = raw_making
251+
252+
if output_decimals > 0:
253+
taking = raw_taking / (Decimal(10) ** output_decimals)
254+
else:
255+
taking = raw_taking
256+
242257
# Calculate USD values at current prices
243258
making_usd = making * input_price
244259
taking_usd_at_current = taking * output_price
@@ -350,27 +365,27 @@ def get_schema(self) -> Dict[str, Any]:
350365
},
351366
"input_decimals": {
352367
"type": "integer",
353-
"description": "Input token decimals (for 'limit_order'). Get from Birdeye. Pass 0 if not needed.",
368+
"description": "Input token decimals (for 'limit_order', 'limit_order_info'). Get from Birdeye. E.g., 9 for SOL, 5 for BONK. REQUIRED for limit_order_info.",
354369
},
355370
"output_price_usd": {
356371
"type": "string",
357372
"description": "Output token price in USD (for 'limit_order', 'limit_order_info'). Get from Birdeye. Pass empty string if not needed.",
358373
},
359374
"output_decimals": {
360375
"type": "integer",
361-
"description": "Output token decimals (for 'limit_order'). Get from Birdeye. Pass 0 if not needed.",
376+
"description": "Output token decimals (for 'limit_order', 'limit_order_info'). Get from Birdeye. E.g., 9 for SOL, 5 for BONK. REQUIRED for limit_order_info.",
362377
},
363378
"price_change_percentage": {
364379
"type": "string",
365380
"description": "Price change percentage for limit order (for 'limit_order'). E.g., '-0.5' for 0.5% lower (buy dip), '10' for 10% higher (sell high). Pass '0' for current price.",
366381
},
367382
"making_amount": {
368383
"type": "string",
369-
"description": "Human-readable amount of input token being sold (for 'limit_order_info'). From order's makingAmount field. Pass empty string if not needed.",
384+
"description": "Amount of input token being sold (for 'limit_order_info'). From order's rawMakingAmount field (smallest units). Pass empty string if not needed.",
370385
},
371386
"taking_amount": {
372387
"type": "string",
373-
"description": "Human-readable amount of output token to receive (for 'limit_order_info'). From order's takingAmount field. Pass empty string if not needed.",
388+
"description": "Amount of output token to receive (for 'limit_order_info'). From order's rawTakingAmount field (smallest units). Pass empty string if not needed.",
374389
},
375390
},
376391
"required": [
@@ -535,11 +550,19 @@ async def execute(
535550
"status": "error",
536551
"message": "Missing required params for 'limit_order_info': making_amount, taking_amount, input_price_usd, output_price_usd",
537552
}
553+
# Validate that decimals are provided (should be > 0)
554+
if input_decimals == 0 or output_decimals == 0:
555+
return {
556+
"status": "error",
557+
"message": "Missing required params for 'limit_order_info': input_decimals and output_decimals must be > 0. Get these from Birdeye. SOL=9, USDC=6, BONK=5.",
558+
}
538559
result = calculate_limit_order_info(
539560
making_amount=making_amount,
540561
taking_amount=taking_amount,
541562
input_price_usd=input_price_usd,
542563
output_price_usd=output_price_usd,
564+
input_decimals=input_decimals,
565+
output_decimals=output_decimals,
543566
)
544567
return {
545568
"status": "success",

tests/test_token_math_tool.py

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -285,14 +285,17 @@ class TestCalculateLimitOrderInfo:
285285

286286
def test_order_at_current_price(self):
287287
"""Should calculate info for order at current market price."""
288-
# Order: sell 0.036 SOL for 521714 BONK
288+
# Order: sell 0.036001982 SOL for 521714.66282 BONK
289+
# Raw amounts: 36001982 lamports, 52171466282 smallest units
289290
result = calculate_limit_order_info(
290-
making_amount="0.036001982",
291-
taking_amount="521714.66282",
291+
making_amount="36001982", # 0.036001982 SOL in lamports
292+
taking_amount="52171466282", # 521714.66282 BONK in smallest units (5 decimals)
292293
input_price_usd="139", # Current SOL price
293294
output_price_usd="0.0000096", # Current BONK price
295+
input_decimals=9, # SOL decimals
296+
output_decimals=5, # BONK decimals
294297
)
295-
# making_usd = 0.036 * 139 = ~$5
298+
# making_usd = 0.036001982 * 139 = ~$5.00
296299
assert float(result["making_usd"]) == pytest.approx(5.00, rel=0.01)
297300
# trigger_price = making_usd / taking_amount = $5 / 521714 = ~$0.00000958
298301
trigger_price = float(result["trigger_price_usd"])
@@ -303,11 +306,14 @@ def test_order_at_current_price(self):
303306

304307
def test_order_should_fill(self):
305308
"""Should detect when order should fill (price dropped to trigger)."""
309+
# Raw: 36000000 lamports = 0.036 SOL, 52171400000 = 521714 BONK
306310
result = calculate_limit_order_info(
307-
making_amount="0.036",
308-
taking_amount="521714",
311+
making_amount="36000000", # 0.036 SOL in lamports
312+
taking_amount="52171400000", # 521714 BONK in smallest units
309313
input_price_usd="139", # SOL price
310314
output_price_usd="0.0000090", # BONK price dropped below trigger
315+
input_decimals=9,
316+
output_decimals=5,
311317
)
312318
# trigger_price = $5 / 521714 = ~$0.00000958
313319
# Current price = $0.0000090 which is BELOW trigger
@@ -316,11 +322,14 @@ def test_order_should_fill(self):
316322

317323
def test_price_difference_calculation(self):
318324
"""Should calculate price difference correctly."""
325+
# Raw: 70000000 lamports = 0.07 SOL, 1000000 * 10^5 = 100000000000 smallest units
319326
result = calculate_limit_order_info(
320-
making_amount="0.07", # ~$10 at $140/SOL
321-
taking_amount="1000000", # 1M BONK
327+
making_amount="70000000", # 0.07 SOL in lamports
328+
taking_amount="100000000000", # 1M BONK in smallest units (5 decimals)
322329
input_price_usd="140",
323330
output_price_usd="0.00001", # Current BONK price
331+
input_decimals=9,
332+
output_decimals=5,
324333
)
325334
# trigger_price = (0.07 * 140) / 1000000 = $0.0000098 per BONK
326335
trigger_price = float(result["trigger_price_usd"])
@@ -332,11 +341,14 @@ def test_price_difference_calculation(self):
332341

333342
def test_usd_values(self):
334343
"""Should calculate USD values correctly."""
344+
# Raw: 1 SOL = 1000000000 lamports, 10000 USDC = 10000000000 (6 decimals)
335345
result = calculate_limit_order_info(
336-
making_amount="1", # 1 SOL
337-
taking_amount="10000", # 10,000 USDC
346+
making_amount="1000000000", # 1 SOL in lamports
347+
taking_amount="10000000000", # 10,000 USDC in smallest units (6 decimals)
338348
input_price_usd="140",
339349
output_price_usd="1", # USDC
350+
input_decimals=9,
351+
output_decimals=6,
340352
)
341353
# making_usd = 1 * 140 = $140
342354
assert result["making_usd"] == "140"
@@ -348,25 +360,46 @@ def test_usd_values(self):
348360
def test_zero_taking_amount(self):
349361
"""Should handle zero taking amount (edge case)."""
350362
result = calculate_limit_order_info(
351-
making_amount="1",
363+
making_amount="1000000000", # 1 SOL in lamports
352364
taking_amount="0", # Zero output
353365
input_price_usd="140",
354366
output_price_usd="1",
367+
input_decimals=9,
368+
output_decimals=6,
355369
)
356370
# trigger_price should be 0 when taking is 0
357371
assert result["trigger_price_usd"] == "0"
358372

359373
def test_zero_output_price(self):
360374
"""Should handle zero output price (edge case)."""
361375
result = calculate_limit_order_info(
362-
making_amount="1",
363-
taking_amount="1000",
376+
making_amount="1000000000", # 1 SOL in lamports
377+
taking_amount="1000000000", # 1000 tokens (6 decimals)
364378
input_price_usd="140",
365379
output_price_usd="0", # Zero price
380+
input_decimals=9,
381+
output_decimals=6,
366382
)
367383
# price_diff should be 0 when output_price is 0
368384
assert result["price_difference_percent"] == "0"
369385

386+
def test_human_readable_amounts_no_decimals(self):
387+
"""Should accept human-readable amounts when decimals are 0 (backwards compatibility)."""
388+
# When decimals=0, amounts are treated as already human-readable
389+
result = calculate_limit_order_info(
390+
making_amount="0.036", # Already human-readable
391+
taking_amount="521714", # Already human-readable
392+
input_price_usd="139",
393+
output_price_usd="0.0000096",
394+
input_decimals=0, # No conversion
395+
output_decimals=0, # No conversion
396+
)
397+
# making_usd = 0.036 * 139 = ~$5
398+
assert float(result["making_usd"]) == pytest.approx(5.00, rel=0.01)
399+
# Check trigger price is calculated correctly
400+
trigger_price = float(result["trigger_price_usd"])
401+
assert trigger_price == pytest.approx(0.00000958, rel=0.01)
402+
370403
def test_invalid_amounts(self):
371404
"""Should raise error for invalid amounts."""
372405
with pytest.raises(ValueError):
@@ -375,6 +408,8 @@ def test_invalid_amounts(self):
375408
taking_amount="100",
376409
input_price_usd="140",
377410
output_price_usd="1",
411+
input_decimals=9,
412+
output_decimals=6,
378413
)
379414

380415

@@ -591,6 +626,7 @@ class TestTokenMathToolExecuteLimitOrderInfo:
591626
@pytest.mark.asyncio
592627
async def test_limit_order_info_success(self, math_tool):
593628
"""Should calculate order info correctly."""
629+
# Raw amounts: 36000000 lamports = 0.036 SOL, 52171400000 = 521714 BONK
594630
result = await math_tool.execute(
595631
action="limit_order_info",
596632
usd_amount="",
@@ -599,16 +635,16 @@ async def test_limit_order_info_success(self, math_tool):
599635
human_amount="",
600636
smallest_units="",
601637
input_price_usd="139", # SOL current price
602-
input_decimals=0,
638+
input_decimals=9, # SOL decimals
603639
output_price_usd="0.0000096", # BONK current price
604-
output_decimals=0,
640+
output_decimals=5, # BONK decimals
605641
price_change_percentage="0",
606-
making_amount="0.036", # Selling 0.036 SOL
607-
taking_amount="521714", # Buying 521k BONK
642+
making_amount="36000000", # 0.036 SOL in lamports
643+
taking_amount="52171400000", # 521714 BONK in smallest units
608644
)
609645
assert result["status"] == "success"
610646
assert result["action"] == "limit_order_info"
611-
# Check USD values
647+
# Check USD values (0.036 * 139 = ~$5)
612648
assert float(result["making_usd"]) == pytest.approx(5.0, rel=0.01)
613649
# Check trigger price
614650
assert "trigger_price_usd" in result
@@ -625,12 +661,12 @@ async def test_limit_order_info_should_fill(self, math_tool):
625661
human_amount="",
626662
smallest_units="",
627663
input_price_usd="139",
628-
input_decimals=0,
664+
input_decimals=9, # SOL decimals
629665
output_price_usd="0.0000090", # Price dropped below trigger
630-
output_decimals=0,
666+
output_decimals=5, # BONK decimals
631667
price_change_percentage="0",
632-
making_amount="0.036",
633-
taking_amount="521714",
668+
making_amount="36000000", # 0.036 SOL in lamports
669+
taking_amount="52171400000", # 521714 BONK in smallest units
634670
)
635671
assert result["status"] == "success"
636672
assert result["should_fill_now"] is True
@@ -655,6 +691,30 @@ async def test_limit_order_info_missing_params(self, math_tool):
655691
)
656692
assert result["status"] == "error"
657693

694+
@pytest.mark.asyncio
695+
async def test_limit_order_info_missing_decimals(self, math_tool):
696+
"""Should return error when decimals are 0."""
697+
result = await math_tool.execute(
698+
action="limit_order_info",
699+
usd_amount="",
700+
token_price_usd="",
701+
decimals=0,
702+
human_amount="",
703+
smallest_units="",
704+
input_price_usd="139",
705+
input_decimals=0, # Missing! Should be 9 for SOL
706+
output_price_usd="0.0000096",
707+
output_decimals=0, # Missing! Should be 5 for BONK
708+
price_change_percentage="0",
709+
making_amount="36000000",
710+
taking_amount="52171400000",
711+
)
712+
assert result["status"] == "error"
713+
assert (
714+
"input_decimals" in result["message"]
715+
or "output_decimals" in result["message"]
716+
)
717+
658718

659719
class TestTokenMathToolExecuteToSmallestUnits:
660720
"""Test 'to_smallest_units' action."""

0 commit comments

Comments
 (0)