Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions intentkit/skills/xmtp/transfer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from decimal import ROUND_DOWN, Decimal, InvalidOperation
from typing import cast, override

from langchain_core.tools import ArgsSchema
from langchain_core.tools.base import ToolException
from pydantic import BaseModel, Field
from web3 import Web3
from web3.exceptions import ContractLogicError

from intentkit.models.chat import ChatMessageAttachment, ChatMessageAttachmentType
Expand All @@ -23,6 +25,25 @@ class TransferInput(BaseModel):
)


def _parse_token_amount(amount: str, decimals: int) -> int:
"""Convert a human-readable token amount to base units exactly."""
try:
decimal_amount = Decimal(amount)
except InvalidOperation:
raise ToolException("Amount must be a valid decimal number")

if not decimal_amount.is_finite() or decimal_amount <= 0:
raise ToolException("Amount must be a positive finite number")

base_units = decimal_amount * (Decimal(10) ** decimals)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Decimal precision when scaling token amounts

For amounts with more than the default 28 significant digits, this multiplication is rounded by Python's active decimal context before the integer check runs; for example, _parse_token_amount("123456789123456789.123456789123456789", 18) produces 123456789123456789123456789100000000 instead of 123456789123456789123456789123456789. Because that rounded value is then embedded in the wallet_sendCalls payload, large or high-precision transfers can silently request the wrong base-unit amount while still passing validation.

Useful? React with 👍 / 👎.

if base_units != base_units.to_integral_value(rounding=ROUND_DOWN):
raise ToolException(
f"Amount has more decimal places than supported by token decimals ({decimals})"
)

return int(base_units)


class XmtpTransfer(XmtpBaseTool):
"""Skill for creating XMTP transfer transactions."""

Expand Down Expand Up @@ -126,7 +147,7 @@ async def _arun(
)

# Calculate amount in smallest unit (wei for ETH, token units for ERC20)
amount_int = int(float(amount) * (10**decimals))
amount_int = _parse_token_amount(amount, decimals)

if token_contract_address:
# ERC20 Token Transfer
Expand All @@ -139,7 +160,7 @@ async def _arun(
method_id = "0xa9059cbb" # transfer(address,uint256) method ID

# Encode to_address (32 bytes, left-padded)
to_address_clean = to_address.replace("0x", "")
to_address_clean = Web3.to_checksum_address(to_address).replace("0x", "")
to_address_padded = to_address_clean.zfill(64)

# Encode amount (32 bytes, left-padded)
Expand Down
22 changes: 22 additions & 0 deletions tests/skills/test_xmtp_transfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest
from langchain_core.tools.base import ToolException

from intentkit.skills.xmtp.transfer import _parse_token_amount


def test_parse_token_amount_preserves_large_integer_precision():
assert (
_parse_token_amount("123456789123456789.123456789123456789", 18)
== 123456789123456789123456789123456789
)


@pytest.mark.parametrize("amount", ["1e309", "Infinity", "NaN", "0", "-1"])
def test_parse_token_amount_rejects_non_positive_or_non_finite_values(amount):
with pytest.raises(ToolException):
_parse_token_amount(amount, 18)


def test_parse_token_amount_rejects_more_precision_than_token_supports():
with pytest.raises(ToolException):
_parse_token_amount("1.0000001", 6)