|
1 | 1 | """OPG token Permit2 approval utilities for x402 payments.""" |
2 | 2 |
|
3 | | -from dataclasses import dataclass |
| 3 | +import logging |
4 | 4 | import time |
| 5 | +import warnings |
| 6 | +from dataclasses import dataclass |
5 | 7 | from typing import Optional |
6 | 8 |
|
7 | 9 | from eth_account.account import LocalAccount |
8 | 10 | from web3 import Web3 |
9 | 11 | from x402.mechanisms.evm.constants import PERMIT2_ADDRESS |
10 | 12 |
|
| 13 | +logger = logging.getLogger(__name__) |
| 14 | + |
11 | 15 | BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F" |
12 | 16 | BASE_SEPOLIA_RPC = "https://sepolia.base.org" |
13 | 17 | APPROVAL_TX_TIMEOUT = 120 |
@@ -53,42 +57,32 @@ class Permit2ApprovalResult: |
53 | 57 | tx_hash: Optional[str] = None |
54 | 58 |
|
55 | 59 |
|
56 | | -def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: |
57 | | - """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. |
58 | | -
|
59 | | - Checks the current Permit2 allowance for the wallet. If the allowance |
60 | | - is already >= the requested amount, returns immediately without sending |
61 | | - a transaction. Otherwise, sends an ERC-20 approve transaction. |
| 60 | +def _send_approve_tx( |
| 61 | + wallet_account: LocalAccount, |
| 62 | + w3: Web3, |
| 63 | + token, |
| 64 | + owner: str, |
| 65 | + spender: str, |
| 66 | + amount_base: int, |
| 67 | +) -> Permit2ApprovalResult: |
| 68 | + """Send an ERC-20 approve transaction and wait for confirmation. |
62 | 69 |
|
63 | 70 | Args: |
64 | | - wallet_account: The wallet account to check and approve from. |
65 | | - opg_amount: Minimum number of OPG tokens required (e.g. ``5.0`` |
66 | | - for 5 OPG). Converted to base units (18 decimals) internally. |
| 71 | + wallet_account: The wallet to sign the transaction with. |
| 72 | + w3: Web3 instance connected to the RPC. |
| 73 | + token: The ERC-20 contract instance. |
| 74 | + owner: Checksummed owner address. |
| 75 | + spender: Checksummed spender (Permit2) address. |
| 76 | + amount_base: The amount to approve in base units (18 decimals). |
67 | 77 |
|
68 | 78 | Returns: |
69 | | - Permit2ApprovalResult: Contains ``allowance_before``, |
70 | | - ``allowance_after``, and ``tx_hash`` (None when no approval |
71 | | - was needed). |
| 79 | + Permit2ApprovalResult with the before/after allowance and tx hash. |
72 | 80 |
|
73 | 81 | Raises: |
74 | | - RuntimeError: If the approval transaction fails. |
| 82 | + RuntimeError: If the transaction reverts or fails. |
75 | 83 | """ |
76 | | - amount_base = int(opg_amount * 10**18) |
77 | | - |
78 | | - w3 = Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC)) |
79 | | - token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI) |
80 | | - owner = Web3.to_checksum_address(wallet_account.address) |
81 | | - spender = Web3.to_checksum_address(PERMIT2_ADDRESS) |
82 | | - |
83 | 84 | allowance_before = token.functions.allowance(owner, spender).call() |
84 | 85 |
|
85 | | - # Only approve if the allowance is less than 50% of the requested amount |
86 | | - if allowance_before >= amount_base * 0.5: |
87 | | - return Permit2ApprovalResult( |
88 | | - allowance_before=allowance_before, |
89 | | - allowance_after=allowance_before, |
90 | | - ) |
91 | | - |
92 | 86 | try: |
93 | 87 | approve_fn = token.functions.approve(spender, amount_base) |
94 | 88 | nonce = w3.eth.get_transaction_count(owner, "pending") |
@@ -133,3 +127,151 @@ def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Perm |
133 | 127 | raise |
134 | 128 | except Exception as e: |
135 | 129 | raise RuntimeError(f"Failed to approve Permit2 for OPG: {e}") |
| 130 | + |
| 131 | + |
| 132 | +def _get_web3_and_contract(): |
| 133 | + """Create a Web3 instance and OPG token contract.""" |
| 134 | + w3 = Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC)) |
| 135 | + token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI) |
| 136 | + spender = Web3.to_checksum_address(PERMIT2_ADDRESS) |
| 137 | + return w3, token, spender |
| 138 | + |
| 139 | + |
| 140 | +def approve_opg(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: |
| 141 | + """Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. |
| 142 | +
|
| 143 | + Idempotent: if the current allowance is already >= ``opg_amount``, no |
| 144 | + transaction is sent. |
| 145 | +
|
| 146 | + Best for one-off usage — scripts, notebooks, CLI tools:: |
| 147 | +
|
| 148 | + result = approve_opg(wallet, 5.0) |
| 149 | +
|
| 150 | + Args: |
| 151 | + wallet_account: The wallet account to check and approve from. |
| 152 | + opg_amount: Number of OPG tokens to approve (e.g. ``5.0`` for 5 OPG). |
| 153 | + Converted to base units (18 decimals) internally. |
| 154 | +
|
| 155 | + Returns: |
| 156 | + Permit2ApprovalResult: Contains ``allowance_before``, |
| 157 | + ``allowance_after``, and ``tx_hash`` (None when no approval |
| 158 | + was needed). |
| 159 | +
|
| 160 | + Raises: |
| 161 | + RuntimeError: If the approval transaction fails. |
| 162 | + """ |
| 163 | + amount_base = int(opg_amount * 10**18) |
| 164 | + |
| 165 | + w3, token, spender = _get_web3_and_contract() |
| 166 | + owner = Web3.to_checksum_address(wallet_account.address) |
| 167 | + |
| 168 | + allowance_before = token.functions.allowance(owner, spender).call() |
| 169 | + |
| 170 | + if allowance_before >= amount_base: |
| 171 | + logger.debug("Permit2 allowance already sufficient (%s >= %s), skipping approval", allowance_before, amount_base) |
| 172 | + return Permit2ApprovalResult( |
| 173 | + allowance_before=allowance_before, |
| 174 | + allowance_after=allowance_before, |
| 175 | + ) |
| 176 | + |
| 177 | + logger.info("Permit2 allowance insufficient (%s < %s), sending approval tx", allowance_before, amount_base) |
| 178 | + return _send_approve_tx(wallet_account, w3, token, owner, spender, amount_base) |
| 179 | + |
| 180 | + |
| 181 | +def ensure_opg_allowance( |
| 182 | + wallet_account: LocalAccount, |
| 183 | + min_allowance: float, |
| 184 | + approve_amount: Optional[float] = None, |
| 185 | +) -> Permit2ApprovalResult: |
| 186 | + """Ensure the Permit2 allowance stays above a minimum threshold. |
| 187 | +
|
| 188 | + Only sends an approval transaction when the current allowance drops |
| 189 | + below ``min_allowance``. When approval is needed, approves |
| 190 | + ``approve_amount`` (defaults to ``10 * min_allowance``) to create a |
| 191 | + buffer that survives multiple service restarts without re-approving. |
| 192 | +
|
| 193 | + Best for backend servers that call this on startup:: |
| 194 | +
|
| 195 | + # On startup — only sends a tx when allowance < 5 OPG, |
| 196 | + # then approves 100 OPG so subsequent restarts are free. |
| 197 | + result = ensure_opg_allowance(wallet, min_allowance=5.0, approve_amount=100.0) |
| 198 | +
|
| 199 | + Args: |
| 200 | + wallet_account: The wallet account to check and approve from. |
| 201 | + min_allowance: The minimum acceptable allowance in OPG. A |
| 202 | + transaction is only sent when the current allowance is |
| 203 | + strictly below this value. |
| 204 | + approve_amount: The amount of OPG to approve when a transaction |
| 205 | + is needed. Defaults to ``10 * min_allowance``. Must be |
| 206 | + >= ``min_allowance``. |
| 207 | +
|
| 208 | + Returns: |
| 209 | + Permit2ApprovalResult: Contains ``allowance_before``, |
| 210 | + ``allowance_after``, and ``tx_hash`` (None when no approval |
| 211 | + was needed). |
| 212 | +
|
| 213 | + Raises: |
| 214 | + ValueError: If ``approve_amount`` is less than ``min_allowance``. |
| 215 | + RuntimeError: If the approval transaction fails. |
| 216 | + """ |
| 217 | + if approve_amount is None: |
| 218 | + approve_amount = min_allowance * 10 |
| 219 | + |
| 220 | + if approve_amount < min_allowance: |
| 221 | + raise ValueError(f"approve_amount ({approve_amount}) must be >= min_allowance ({min_allowance})") |
| 222 | + |
| 223 | + min_base = int(min_allowance * 10**18) |
| 224 | + approve_base = int(approve_amount * 10**18) |
| 225 | + |
| 226 | + w3, token, spender = _get_web3_and_contract() |
| 227 | + owner = Web3.to_checksum_address(wallet_account.address) |
| 228 | + |
| 229 | + allowance_before = token.functions.allowance(owner, spender).call() |
| 230 | + |
| 231 | + if allowance_before >= min_base: |
| 232 | + logger.debug( |
| 233 | + "Permit2 allowance above minimum threshold (%s >= %s), skipping approval", |
| 234 | + allowance_before, |
| 235 | + min_base, |
| 236 | + ) |
| 237 | + return Permit2ApprovalResult( |
| 238 | + allowance_before=allowance_before, |
| 239 | + allowance_after=allowance_before, |
| 240 | + ) |
| 241 | + |
| 242 | + logger.info( |
| 243 | + "Permit2 allowance below minimum threshold (%s < %s), approving %s base units", |
| 244 | + allowance_before, |
| 245 | + min_base, |
| 246 | + approve_base, |
| 247 | + ) |
| 248 | + return _send_approve_tx(wallet_account, w3, token, owner, spender, approve_base) |
| 249 | + |
| 250 | + |
| 251 | +def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: |
| 252 | + """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. |
| 253 | +
|
| 254 | + .. deprecated:: |
| 255 | + Use ``approve_opg`` for one-off approvals or |
| 256 | + ``ensure_opg_allowance`` for server-startup usage. |
| 257 | +
|
| 258 | + Args: |
| 259 | + wallet_account: The wallet account to check and approve from. |
| 260 | + opg_amount: Minimum number of OPG tokens required (e.g. ``5.0`` |
| 261 | + for 5 OPG). Converted to base units (18 decimals) internally. |
| 262 | +
|
| 263 | + Returns: |
| 264 | + Permit2ApprovalResult: Contains ``allowance_before``, |
| 265 | + ``allowance_after``, and ``tx_hash`` (None when no approval |
| 266 | + was needed). |
| 267 | +
|
| 268 | + Raises: |
| 269 | + RuntimeError: If the approval transaction fails. |
| 270 | + """ |
| 271 | + warnings.warn( |
| 272 | + "ensure_opg_approval is deprecated. Use approve_opg for one-off approvals " |
| 273 | + "or ensure_opg_allowance for server-startup usage.", |
| 274 | + DeprecationWarning, |
| 275 | + stacklevel=2, |
| 276 | + ) |
| 277 | + return approve_opg(wallet_account, opg_amount) |
0 commit comments