Skip to content
Closed
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
4 changes: 2 additions & 2 deletions e2e/facilitators/text-facilitator-protocol.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Example configuration:
}
```

Python facilitator example (eip3009 only):
Python facilitator example (eip3009 + permit2):
```json
{
"name": "python",
Expand All @@ -57,7 +57,7 @@ Python facilitator example (eip3009 only):
"x402Versions": [2],
"extensions": ["bazaar"],
"evm": {
"transferMethods": ["eip3009"]
"transferMethods": ["eip3009", "permit2"]
},
"environment": {
"required": ["PORT", "EVM_PRIVATE_KEY", "SVM_PRIVATE_KEY", "APTOS_PRIVATE_KEY"],
Expand Down
6 changes: 0 additions & 6 deletions e2e/src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,6 @@ export class TestDiscovery {
});

for (const facilitator of matchingFacilitators) {
// TODO: Python SDK currently lacks Permit2 support.
// We skip these scenarios when using the python facilitator to avoid expected failures.
if (facilitator.name === 'python' && (endpoint.transferMethod === 'permit2' || (endpoint as any).permit2)) {
continue;
}

scenarios.push({
client,
server,
Expand Down
26 changes: 26 additions & 0 deletions python/x402/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ uv add "bankofai.x402[all]"

## Quick Start

### EVM Transfer Methods

The Python SDK supports both EVM transfer methods used by the TypeScript SDK:

- `eip3009` for tokens that implement `transferWithAuthorization`
- `permit2` for networks and assets configured to use Permit2 witness settlement

On BSC mainnet and testnet, the default stablecoin route uses `permit2`. The payer
wallet must pre-approve the configured Permit2 contract before settlement.

### Client (Async)

```python
Expand Down Expand Up @@ -264,6 +274,22 @@ client.register("eip155:8453", CustomScheme())
- `x402.mechanisms.svm` - Solana implementation
- `x402.extensions` - Protocol extensions (Bazaar discovery)

## Integration Tests

The Python integration suite includes:

- Base Sepolia `eip3009`
- BSC Testnet `permit2`

For BSC Testnet `permit2` integration tests, set:

- `BSC_CLIENT_PRIVATE_KEY`
- `BSC_FACILITATOR_PRIVATE_KEY`
- `BSC_TESTNET_RPC_URL`

The payer wallet must also hold testnet BNB, testnet USDT, and a token approval for
the configured BSC Permit2 contract.

## Examples

See [examples/python](https://github.com/coinbase/x402/tree/main/examples/python).
32 changes: 32 additions & 0 deletions python/x402/src/bankofai/x402/mechanisms/evm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@
ERR_INSUFFICIENT_BALANCE,
ERR_INVALID_SIGNATURE,
ERR_MISSING_EIP712_DOMAIN,
ERR_MISSING_PERMIT2_ADDRESS,
ERR_NETWORK_MISMATCH,
ERR_NONCE_ALREADY_USED,
ERR_PERMIT2_ALLOWANCE_REQUIRED,
ERR_PERMIT2_AMOUNT_MISMATCH,
ERR_PERMIT2_DEADLINE_EXPIRED,
ERR_PERMIT2_INVALID_SIGNATURE,
ERR_PERMIT2_NOT_YET_VALID,
ERR_PERMIT2_RECIPIENT_MISMATCH,
ERR_PERMIT2_TOKEN_MISMATCH,
ERR_RECIPIENT_MISMATCH,
ERR_SMART_WALLET_DEPLOYMENT_FAILED,
ERR_TRANSACTION_FAILED,
Expand All @@ -26,11 +34,16 @@
ERR_VALID_BEFORE_EXPIRED,
IS_VALID_SIGNATURE_ABI,
NETWORK_CONFIGS,
PERMIT2_ADDRESSES,
PERMIT2_WITNESS_TYPES,
SCHEME_EXACT,
TRANSFER_WITH_AUTHORIZATION_BYTES_ABI,
TRANSFER_WITH_AUTHORIZATION_VRS_ABI,
TX_STATUS_FAILED,
TX_STATUS_SUCCESS,
X402_EXACT_PERMIT2_PROXY_ABI,
X402_PERMIT2_PROXY_ADDRESSES,
X402_UPTO_PERMIT2_PROXY_ADDRESSES,
AssetInfo,
NetworkConfig,
)
Expand Down Expand Up @@ -67,6 +80,9 @@
ExactEIP3009Payload,
ExactEvmPayloadV1,
ExactEvmPayloadV2,
ExactPermit2Authorization,
ExactPermit2Payload,
Permit2Witness,
TransactionReceipt,
TypedDataDomain,
TypedDataField,
Expand Down Expand Up @@ -124,9 +140,17 @@
"ERR_NONCE_ALREADY_USED",
"ERR_INSUFFICIENT_BALANCE",
"ERR_MISSING_EIP712_DOMAIN",
"ERR_MISSING_PERMIT2_ADDRESS",
"ERR_NETWORK_MISMATCH",
"ERR_UNSUPPORTED_SCHEME",
"ERR_TRANSACTION_FAILED",
"ERR_PERMIT2_ALLOWANCE_REQUIRED",
"ERR_PERMIT2_AMOUNT_MISMATCH",
"ERR_PERMIT2_DEADLINE_EXPIRED",
"ERR_PERMIT2_INVALID_SIGNATURE",
"ERR_PERMIT2_NOT_YET_VALID",
"ERR_PERMIT2_RECIPIENT_MISMATCH",
"ERR_PERMIT2_TOKEN_MISMATCH",
"ERR_FAILED_TO_GET_NETWORK_CONFIG",
"ERR_FAILED_TO_GET_ASSET_INFO",
"ERR_FAILED_TO_VERIFY_SIGNATURE",
Expand All @@ -135,13 +159,21 @@
"AUTHORIZATION_STATE_ABI",
"BALANCE_OF_ABI",
"IS_VALID_SIGNATURE_ABI",
"PERMIT2_ADDRESSES",
"PERMIT2_WITNESS_TYPES",
"X402_PERMIT2_PROXY_ADDRESSES",
"X402_UPTO_PERMIT2_PROXY_ADDRESSES",
"X402_EXACT_PERMIT2_PROXY_ABI",
"AssetInfo",
"NetworkConfig",
# Types
"ExactEIP3009Authorization",
"ExactEIP3009Payload",
"ExactPermit2Authorization",
"ExactPermit2Payload",
"ExactEvmPayloadV1",
"ExactEvmPayloadV2",
"Permit2Witness",
"TypedDataDomain",
"TypedDataField",
"TransactionReceipt",
Expand Down
131 changes: 131 additions & 0 deletions python/x402/src/bankofai/x402/mechanisms/evm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
ERR_FAILED_TO_GET_ASSET_INFO = "invalid_exact_evm_failed_to_get_asset_info"
ERR_FAILED_TO_VERIFY_SIGNATURE = "invalid_exact_evm_failed_to_verify_signature"
ERR_TRANSACTION_FAILED = "transaction_failed"
ERR_MISSING_PERMIT2_ADDRESS = "missing_permit2_address"
ERR_INVALID_PERMIT2_SPENDER = "invalid_permit2_spender"
ERR_PERMIT2_RECIPIENT_MISMATCH = "permit2_recipient_mismatch"
ERR_INVALID_PERMIT2_FACILITATOR = "invalid_permit2_facilitator"
ERR_PERMIT2_DEADLINE_EXPIRED = "permit2_deadline_expired"
ERR_PERMIT2_NOT_YET_VALID = "permit2_not_yet_valid"
ERR_PERMIT2_AMOUNT_MISMATCH = "permit2_amount_mismatch"
ERR_PERMIT2_TOKEN_MISMATCH = "permit2_token_mismatch"
ERR_PERMIT2_INVALID_SIGNATURE = "permit2_invalid_signature"
ERR_PERMIT2_ALLOWANCE_REQUIRED = "permit2_allowance_required"


class _AssetInfoRequired(TypedDict):
Expand Down Expand Up @@ -122,6 +132,63 @@ class NetworkConfig(_NetworkConfigRequired, total=False):
"decimals": 6,
},
},
# BSC Mainnet
"eip155:56": {
"chain_id": 56,
"default_asset": {
"address": "0x55d398326f99059fF775485246999027B3197955",
"name": "Tether USD",
"version": "1",
"decimals": 18,
"asset_transfer_method": "permit2",
},
},
# BSC Testnet
"eip155:97": {
"chain_id": 97,
"default_asset": {
"address": "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
"name": "Tether USD",
"version": "1",
"decimals": 18,
"asset_transfer_method": "permit2",
},
},
}

PERMIT2_ADDRESSES: dict[str, str] = {
"eip155:1": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
"eip155:56": "0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768",
"eip155:97": "0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768",
}

X402_PERMIT2_PROXY_ADDRESSES: dict[str, str] = {
"eip155:56": "0xEe38Ec718255fe78e9D16aCC0e1183C731679b23",
"eip155:97": "0xEe38Ec718255fe78e9D16aCC0e1183C731679b23",
}

X402_UPTO_PERMIT2_PROXY_ADDRESSES: dict[str, str] = {
"eip155:56": "0x2b30Ed9F37c7C21ae8779c5753B1cCf264DfD63C",
"eip155:97": "0x2b30Ed9F37c7C21ae8779c5753B1cCf264DfD63C",
}

PERMIT2_WITNESS_TYPES: dict[str, list[dict[str, str]]] = {
"PermitWitnessTransferFrom": [
{"name": "permitted", "type": "TokenPermissions"},
{"name": "spender", "type": "address"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"},
{"name": "witness", "type": "Witness"},
],
"TokenPermissions": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"Witness": [
{"name": "to", "type": "address"},
{"name": "facilitator", "type": "address"},
{"name": "validAfter", "type": "uint256"},
],
}

# V1 legacy constants are in x402.mechanisms.evm.v1.constants
Expand Down Expand Up @@ -179,6 +246,70 @@ class NetworkConfig(_NetworkConfigRequired, total=False):
}
]

ERC20_ALLOWANCE_ABI = [
{
"inputs": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
],
"name": "allowance",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
}
]

ERC20_APPROVE_ABI = [
{
"inputs": [
{"name": "spender", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
}
]

X402_EXACT_PERMIT2_PROXY_ABI = [
{
"inputs": [
{
"name": "permit",
"type": "tuple",
"components": [
{
"name": "permitted",
"type": "tuple",
"components": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint256"},
],
},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"},
],
},
{"name": "owner", "type": "address"},
{
"name": "witness",
"type": "tuple",
"components": [
{"name": "to", "type": "address"},
{"name": "facilitator", "type": "address"},
{"name": "validAfter", "type": "uint256"},
],
},
{"name": "signature", "type": "bytes"},
],
"name": "settle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
}
]

BALANCE_OF_ABI = [
{
"inputs": [{"name": "account", "type": "address"}],
Expand Down
25 changes: 21 additions & 4 deletions python/x402/src/bankofai/x402/mechanisms/evm/eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from .types import (
AUTHORIZATION_TYPES,
DOMAIN_TYPES,
ExactEIP3009Authorization,
TypedDataDomain,
)
Expand Down Expand Up @@ -135,11 +134,20 @@ def hash_domain(domain: TypedDataDomain) -> bytes:
"""
domain_data = {
"name": domain.name,
"version": domain.version,
"chainId": domain.chain_id,
"verifyingContract": domain.verifying_contract,
}
return hash_struct("EIP712Domain", DOMAIN_TYPES, domain_data)
domain_types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
]
}
if domain.version is not None:
domain_data["version"] = domain.version
domain_types["EIP712Domain"].insert(1, {"name": "version", "type": "string"})
return hash_struct("EIP712Domain", domain_types, domain_data)


def hash_typed_data(
Expand All @@ -162,7 +170,16 @@ def hash_typed_data(
32-byte hash suitable for signing/verification.
"""
# Merge domain types with provided types
all_types = {**DOMAIN_TYPES, **types}
domain_types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
]
}
if domain.version is not None:
domain_types["EIP712Domain"].insert(1, {"name": "version", "type": "string"})
all_types = {**domain_types, **types}

domain_separator = hash_domain(domain)
struct_hash = hash_struct(primary_type, all_types, message)
Expand Down
Loading
Loading