Skip to content

Commit 8aa39fb

Browse files
author
balogh.adam@icloud.com
committed
opg management
1 parent e5dffc6 commit 8aa39fb

2 files changed

Lines changed: 171 additions & 29 deletions

File tree

src/opengradient/client/llm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from x402.mechanisms.evm.upto.register import register_upto_evm_client
1616

1717
from ..types import TEE_LLM, StreamChoice, StreamChunk, StreamDelta, TextGenerationOutput, x402SettlementMode
18-
from .opg_token import Permit2ApprovalResult, ensure_opg_approval
18+
from .opg_token import Permit2ApprovalResult, approve_opg, ensure_opg_allowance, ensure_opg_approval
1919
from .tee_connection import RegistryTEEConnection, StaticTEEConnection, TEEConnectionInterface
2020
from .tee_registry import TEERegistry
2121

src/opengradient/client/opg_token.py

Lines changed: 170 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"""OPG token Permit2 approval utilities for x402 payments."""
22

3-
from dataclasses import dataclass
3+
import logging
44
import time
5+
import warnings
6+
from dataclasses import dataclass
57
from typing import Optional
68

79
from eth_account.account import LocalAccount
810
from web3 import Web3
911
from x402.mechanisms.evm.constants import PERMIT2_ADDRESS
1012

13+
logger = logging.getLogger(__name__)
14+
1115
BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F"
1216
BASE_SEPOLIA_RPC = "https://sepolia.base.org"
1317
APPROVAL_TX_TIMEOUT = 120
@@ -53,42 +57,32 @@ class Permit2ApprovalResult:
5357
tx_hash: Optional[str] = None
5458

5559

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.
6269
6370
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).
6777
6878
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.
7280
7381
Raises:
74-
RuntimeError: If the approval transaction fails.
82+
RuntimeError: If the transaction reverts or fails.
7583
"""
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-
8384
allowance_before = token.functions.allowance(owner, spender).call()
8485

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-
9286
try:
9387
approve_fn = token.functions.approve(spender, amount_base)
9488
nonce = w3.eth.get_transaction_count(owner, "pending")
@@ -133,3 +127,151 @@ def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Perm
133127
raise
134128
except Exception as e:
135129
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

Comments
 (0)