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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add unit tests for `sign_eip2612_permit` in the EIP-2612 gas sponsoring extension covering nonce read, EIP-712 domain/types/message construction, and return shape (mirrors the `sign_erc20_approval_transaction` test pattern).
168 changes: 167 additions & 1 deletion python/x402/tests/unit/extensions/test_eip2612_gas_sponsoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
validate_eip2612_gas_sponsoring_info,
validate_eip2612_permit_for_payment,
)
from x402.mechanisms.evm.constants import PERMIT2_ADDRESS
from x402.extensions.eip2612_gas_sponsoring.client import sign_eip2612_permit
from x402.mechanisms.evm.constants import (
EIP2612_NONCES_ABI,
EIP2612_PERMIT_TYPES,
PERMIT2_ADDRESS,
)
from x402.mechanisms.evm.types import TypedDataField
from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo

TOKEN_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
Expand Down Expand Up @@ -141,3 +147,163 @@ def test_spender_not_permit2(self):
def test_expired_deadline(self):
info = _make_info(deadline=str(int(time.time()) - 100))
assert "deadline_expired" in validate_eip2612_permit_for_payment(info, PAYER, TOKEN_ADDRESS)


class _StubPermitSigner:
"""Minimal signer that records every read_contract / sign_typed_data call.

Mirrors the stub-signer pattern used in test_erc20_approval_gas_sponsoring.py
for sign_erc20_approval_transaction.
"""

def __init__(self, nonce: int = 0, signature: bytes | None = None):
self.address = PAYER
self._nonce = nonce
self._signature = signature if signature is not None else b"\xab" * 65
self.read_contract_calls: list[tuple[Any, ...]] = []
self.sign_typed_data_calls: list[dict[str, Any]] = []

def read_contract(
self,
address: str,
abi: list[dict[str, Any]],
function_name: str,
*args: Any,
) -> int:
self.read_contract_calls.append((address, abi, function_name, args))
return self._nonce

def sign_typed_data(
self,
domain: dict[str, Any],
types: dict[str, list[TypedDataField]],
primary_type: str,
message: dict[str, Any],
) -> bytes:
self.sign_typed_data_calls.append(
{
"domain": domain,
"types": types,
"primary_type": primary_type,
"message": message,
}
)
return self._signature


class TestSignEip2612Permit:
"""Coverage for sign_eip2612_permit: nonce read, EIP-712 payload, return shape."""

def test_returns_info_with_all_fields_populated(self):
signer = _StubPermitSigner(nonce=7, signature=b"\xcd" * 65)
deadline = "1700000000"
amount = "1000000"

info = sign_eip2612_permit(
signer=signer,
token_address=TOKEN_ADDRESS,
token_name="USDC",
token_version="2",
chain_id=84532,
deadline=deadline,
amount=amount,
)

assert info.from_address == PAYER
assert info.asset == TOKEN_ADDRESS
assert info.spender == PERMIT2_ADDRESS
assert info.amount == amount
assert info.deadline == deadline
assert info.nonce == "7"
assert info.signature == "0x" + ("cd" * 65)
assert info.version == "1"

def test_reads_nonce_from_token_using_signer_address(self):
signer = _StubPermitSigner(nonce=42)

sign_eip2612_permit(
signer=signer,
token_address=TOKEN_ADDRESS,
token_name="USDC",
token_version="2",
chain_id=84532,
deadline="1700000000",
amount="1000",
)

assert len(signer.read_contract_calls) == 1
address, abi, function_name, args = signer.read_contract_calls[0]
assert address == TOKEN_ADDRESS
assert abi == EIP2612_NONCES_ABI
assert function_name == "nonces"
assert args == (PAYER,)

def test_signs_typed_data_with_correct_domain_and_primary_type(self):
signer = _StubPermitSigner()

sign_eip2612_permit(
signer=signer,
token_address=TOKEN_ADDRESS,
token_name="USDC",
token_version="2",
chain_id=84532,
deadline="1700000000",
amount="1000",
)

assert len(signer.sign_typed_data_calls) == 1
call = signer.sign_typed_data_calls[0]
assert call["primary_type"] == "Permit"
assert call["domain"] == {
"name": "USDC",
"version": "2",
"chainId": 84532,
"verifyingContract": TOKEN_ADDRESS,
}
# Types must mirror EIP2612_PERMIT_TYPES in name and order, just as
# TypedDataField objects rather than raw dicts.
expected_types = {
type_name: [TypedDataField(name=f["name"], type=f["type"]) for f in fields]
for type_name, fields in EIP2612_PERMIT_TYPES.items()
}
assert call["types"] == expected_types

def test_signs_message_with_int_fields_and_correct_owner_spender(self):
signer = _StubPermitSigner(nonce=3)

sign_eip2612_permit(
signer=signer,
token_address=TOKEN_ADDRESS,
token_name="USDC",
token_version="2",
chain_id=84532,
deadline="1700000000",
amount="1500",
)

message = signer.sign_typed_data_calls[0]["message"]
assert message["owner"] == PAYER
assert message["spender"] == PERMIT2_ADDRESS
# Numeric fields are coerced to int for EIP-712 signing even though
# the public API takes them as decimal strings.
assert message["value"] == 1500
assert message["nonce"] == 3
assert message["deadline"] == 1700000000
assert all(isinstance(message[k], int) for k in ("value", "nonce", "deadline"))

def test_supports_max_uint256_amount(self):
signer = _StubPermitSigner()
max_uint256 = str(2**256 - 1)

info = sign_eip2612_permit(
signer=signer,
token_address=TOKEN_ADDRESS,
token_name="USDC",
token_version="2",
chain_id=84532,
deadline="1700000000",
amount=max_uint256,
)

assert info.amount == max_uint256
assert signer.sign_typed_data_calls[0]["message"]["value"] == 2**256 - 1
Loading